Compare commits

...
61 Commits
Author SHA1 Message Date
yyc12345 4fcdf902f7 fix: use rust raw string literal syntax for quote mark escape 2026-06-26 16:04:15 +08:00
yyc12345 e3a1195c04 doc: add the doc for c++ binding according to c binding 2026-06-26 14:59:14 +08:00
yyc12345 db69452213 doc: add docstring for highlevel functions 2026-06-26 14:40:20 +08:00
yyc12345 a350a591ad doc: add docstring for lowlevel 2026-06-26 13:44:56 +08:00
yyc12345 6a354bd7a7 doc: add some note for win32 module test 2026-06-25 21:51:21 +08:00
yyc12345 23d7045ec1 doc: add docstrings for win32 module 2026-06-25 21:47:38 +08:00
yyc12345 3492f93c83 fix: fix the reference to unlink from link 2026-06-25 15:38:36 +08:00
yyc12345 d1de56a21d feat: modify ppic example for the latest ppic 2026-06-25 15:27:41 +08:00
yyc12345 880f90211b feat: update error notes for wfassoc 2026-06-25 13:42:23 +08:00
yyc12345 1a44240f88 feat: update qwfassoc 2026-06-23 21:38:54 +08:00
yyc12345 071347f4d4 feat: include ai first step works 2026-06-23 20:46:56 +08:00
yyc12345 821b865f2d refactor: add WF prefix for c/c++ binding members 2026-06-17 21:40:19 +08:00
yyc12345 50f01429b0 doc: update readme 2026-06-17 21:23:27 +08:00
yyc12345 930a84a0f6 doc: add doc for test instruction 2026-06-01 22:08:25 +08:00
yyc12345 447d94fdd6 fix: add arbitrarily_delete_value and fix various bugs for lowlevel and highlevel 2026-05-29 09:46:59 +08:00
yyc12345 f0bd2c0b73 feat: add arbitrarily_delete_subkey_all for regext 2026-05-29 08:58:15 +08:00
yyc12345 119a4d0341 feat: add highlevel test 2026-05-28 21:34:42 +08:00
yyc12345 53b40a4d2f feat: add test for lowlevel 2026-05-28 21:03:50 +08:00
yyc12345 0533cdab23 test: add test for default icon resource 2026-05-27 19:12:52 +08:00
yyc12345 8c61aa1e1d feat: remove some optional in rust and cbindgen 2026-05-27 13:16:51 +08:00
yyc12345 77924b5937 feat: update cbindgen 2026-05-27 12:30:03 +08:00
yyc12345 2c811503a2 feat: update program and cdylib exposed interface 2026-05-27 10:38:45 +08:00
yyc12345 18a55272a3 ai: update tasks 2026-05-25 23:26:11 +08:00
yyc12345 1658127bdd doc: add some prompt for qwfassoc 2026-05-25 16:34:35 +08:00
yyc12345 72a8c13c1f feat: add c++ wfassoc wrapper 2026-05-19 20:34:12 +08:00
yyc12345 aececd8e5d feat: entirely remove old pure C project 2026-05-19 14:22:45 +08:00
yyc12345 883cba901c feat: updte wfassoc C header 2026-05-19 14:21:01 +08:00
yyc12345 a1874f3682 feat: update wfassoc C header 2026-05-19 12:54:59 +08:00
yyc12345 3c429b6c51 feat: remove all old useless code 2026-05-19 11:04:25 +08:00
yyc12345 a4d2f7bc26 feat: finish all highlevel function and expose them to cdylib 2026-05-19 10:59:57 +08:00
yyc12345 658806e9ff refactor: seperate highlevel into 2 individual files 2026-05-19 10:26:17 +08:00
yyc12345 f8db414da3 fix: fix the behavior of highlevel fetching string and icon 2026-05-18 21:26:02 +08:00
yyc12345 bc419f8d5e fix: fix refstr concept issue 2026-05-18 21:14:26 +08:00
yyc12345 3b0080849d fix: fix expand string error 2026-05-18 20:46:31 +08:00
yyc12345 53cc8edcfd fix: update blank guard usage and fix issue 2026-05-18 17:04:11 +08:00
yyc12345 1086039dcb feat: finish all lowlevel function 2026-05-18 16:40:06 +08:00
yyc12345 ae0231fe69 refactor: seperate lowlevel into 4 individual submodules 2026-05-18 16:27:39 +08:00
yyc12345 4205ef18d3 feat: write some content for lowlevel 2026-05-18 16:00:29 +08:00
yyc12345 bbcea1f4d2 feat: add more guard and const in lowlevel 2026-05-18 15:43:16 +08:00
yyc12345 91c7fdda70 feat: finish cdylib ffi and improve ffi C header 2026-05-18 13:33:57 +08:00
yyc12345 f1a7cb89e5 feat: add ext status for cdylib 2026-05-17 16:02:39 +08:00
yyc12345 fe9f839091 feat: finish default icon and friendly app name for application subkey 2026-05-17 15:34:18 +08:00
yyc12345 9d3467ea26 feat: add application subkey shell verb function 2026-05-17 14:28:29 +08:00
yyc12345 9d02a2187f feat: add some boring function in lowlevel 2026-05-17 12:37:14 +08:00
yyc12345 c164bb73bc feat: add two registry utility functions 2026-05-16 22:35:36 +08:00
yyc12345 92ec83b8d7 feat: remove cbindgen. use manual writing instead (actually AI writing) 2026-05-16 13:58:12 +08:00
yyc12345 8d7f96d499 feat: add cbindgen support 2026-05-15 17:02:29 +08:00
yyc12345 684a24fcf8 feat: update cdylib 2026-05-15 16:00:54 +08:00
yyc12345 5c182bb4d3 feat: add all schema functions in cdylib project 2026-05-11 14:24:28 +08:00
yyc12345 9e9aa282b1 feat: use macro to simplify cdylib decl 2026-05-11 13:54:47 +08:00
yyc12345 76ddddb6cd feat: fix LazyLock issue and add something in cdylib 2026-05-11 13:33:01 +08:00
yyc12345 f0e610f8c8 feat: add example for cdylib 2026-05-10 23:16:56 +08:00
yyc12345 7d92f9a4a0 feat: fix cstr ffi again 2026-05-10 16:55:09 +08:00
yyc12345 1a45b309e7 feat: update cdylib cstr ffi module 2026-05-10 16:43:25 +08:00
yyc12345 7f36375a73 refactor: remove old exec file 2026-05-09 20:10:15 +08:00
yyc12345 a6322bff51 feat: finish exec runner 2026-05-09 20:07:08 +08:00
yyc12345 6804b96078 feat: add object pool for cdylib 2026-05-09 16:27:04 +08:00
yyc12345 d4b52efee0 feat: fix exec runner issue 2026-05-08 14:14:55 +08:00
yyc12345 ec5c78e8ce feat: finish highlevel 2026-05-08 14:08:51 +08:00
yyc12345 e325ba08f1 feat: finish highlevel register 2026-05-08 10:56:47 +08:00
yyc12345 03d17fad6e feat: finish highlevel unregister 2026-05-08 10:45:43 +08:00
83 changed files with 8330 additions and 5066 deletions
Generated
+62 -109
View File
@@ -73,25 +73,6 @@ version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "cbindgen"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "975982cdb7ad6a142be15bdf84aea7ec6a9e5d4d797c004d43185b24cfe4e684"
dependencies = [
"clap",
"heck",
"indexmap",
"log",
"proc-macro2",
"quote",
"serde",
"serde_json",
"syn",
"tempfile",
"toml 0.8.23",
]
[[package]]
name = "cfg-if"
version = "1.0.3"
@@ -203,12 +184,6 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "getrandom"
version = "0.3.4"
@@ -249,12 +224,6 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
version = "0.3.81"
@@ -304,6 +273,28 @@ version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "num_enum"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
dependencies = [
"num_enum_derive",
"rustversion",
]
[[package]]
name = "num_enum_derive"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -339,6 +330,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "proc-macro-crate"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.101"
@@ -420,12 +420,6 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -462,28 +456,6 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "1.0.3"
@@ -493,12 +465,27 @@ dependencies = [
"serde_core",
]
[[package]]
name = "slotmap"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
dependencies = [
"version_check",
]
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "strfmt"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29fdc163db75f7b5ffa3daf0c5a7136fb0d4b2f35523cd1769da05e034159feb"
[[package]]
name = "strsim"
version = "0.11.1"
@@ -516,19 +503,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix",
"windows-sys 0.60.2",
]
[[package]]
name = "thiserror"
version = "2.0.17"
@@ -549,18 +523,6 @@ dependencies = [
"syn",
]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"toml_edit",
]
[[package]]
name = "toml"
version = "0.9.8"
@@ -569,22 +531,13 @@ checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned 1.0.3",
"toml_datetime 0.7.3",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_datetime"
version = "0.7.3"
@@ -596,15 +549,13 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.22.27"
version = "0.23.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832"
dependencies = [
"indexmap",
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"toml_write",
"toml_datetime",
"toml_parser",
"winnow",
]
@@ -617,12 +568,6 @@ dependencies = [
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "toml_writer"
version = "1.0.4"
@@ -664,6 +609,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
@@ -738,6 +689,7 @@ version = "0.1.0"
dependencies = [
"indexmap",
"regex",
"strfmt",
"thiserror",
"uuid",
"widestring",
@@ -749,7 +701,8 @@ dependencies = [
name = "wfassoc-cdylib"
version = "0.1.0"
dependencies = [
"cbindgen",
"num_enum",
"slotmap",
"thiserror",
"wfassoc",
]
@@ -762,7 +715,7 @@ dependencies = [
"comfy-table",
"serde",
"thiserror",
"toml 0.9.8",
"toml",
"wfassoc",
]
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2022-2025 yyc12345
Copyright (c) 2022-2026 yyc12345
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+198 -73
View File
@@ -34,7 +34,7 @@ clsid = "{B5291320-FE7C-4069-BF87-A0AC327FCD20}"
#
# This key is optional. If you don't fill this key,
# Windows will use your executable embedded Win32 resources for friendly name.
name = "default"
name = "self"
# The icon of this program.
#
@@ -42,7 +42,7 @@ name = "default"
#
# This key is optional. If you don't fill this key,
# Windows will use the first icon embedded in your executable as icon.
icon = "others"
icon = "self"
# The default behavior of your program.
#
@@ -69,30 +69,41 @@ behavior = "generic"
#
# Due to the same use of back-slash in path part,
# you can utilize TOML "literal strings" syntax to write this value easier.
default = '@C:\path\to\ppic.exe,-1001'
# self = '@C:\path\to\ppic.exe,-1001'
# The value of this key also can be a plain string like this commente statement,
# if you don't want to touch Win32 resources and your program do not have any I18N requirements.
# default = "Pineapple Picture"
self = "Pineapple Pictures"
# And more string resources...
jpg = '@C:\path\to\ppic.exe,-1011'
gif = '@C:\path\to\ppic.exe,-1012'
bmp = '@C:\path\to\ppic.exe,-1013'
png = '@C:\path\to\ppic.exe,-1014'
ico = '@C:\path\to\ppic.exe,-1015'
jpeg = '@C:\path\to\ppic.exe,-1016'
tif = '@C:\path\to\ppic.exe,-1017'
tiff = '@C:\path\to\ppic.exe,-1018'
webp = '@C:\path\to\ppic.exe,-1019'
avif = '@C:\path\to\ppic.exe,-1020'
svg = '@C:\path\to\ppic.exe,-1021'
kra = '@C:\path\to\ppic.exe,-1022'
xcf = '@C:\path\to\ppic.exe,-1023'
qoi = '@C:\path\to\ppic.exe,-1024'
apng = '@C:\path\to\ppic.exe,-1025'
exr = '@C:\path\to\ppic.exe,-1026'
tga = '@C:\path\to\ppic.exe,-1027'
jpeg = 'JPEG Image'
gif = 'GIF Animation Image'
png = 'PNG Image'
svg = 'Scalable Vector Graphics'
webp = 'WebP Image'
avif = 'AV1 Image File'
ico = 'Windows Icon Image'
icns = 'Apple Icon Image'
psd = 'Photoshop Document'
kra = 'Krita Document'
xcf = 'GIMP Document'
tga = 'TARGA Image'
tif = 'Tagged Image File Format'
bmp = 'Bitmap'
wbmp = 'Wireless Bitmap'
dds = 'DirectDraw Surface'
hdr = 'High Dynamic Range Image'
iff = 'Interchange File Format'
pic = 'Softimage Picture'
pcx = 'PiCture eXchange PC Paintbrush Image'
qoi = 'Quite OK Image'
ras = 'Sun Raster Image'
ora = 'OpenRaster Image'
ani = 'Animated Cursor'
pfm = 'Portable Float Map'
rgb = 'Silicon Graphics Image'
pxr = 'Pixar Raster Image'
sct = 'Scitex Continuous Tone'
# ======== Icon Resources ========
@@ -112,18 +123,17 @@ tga = '@C:\path\to\ppic.exe,-1027'
#
# Due to the same use of back-slash in path part,
# you can utilize TOML "literal strings" syntax to write this value easier.
png = 'C:\path\to\ppic.exe,-1'
self = 'C:\path\to\ppic.exe,0'
# The value of this key also can be the absolute path to existing icon file like this commente statement.
# png = 'C:\path\to\ppic\icons\png.ico'
generic = 'C:\path\to\ppic\icons\generic.ico'
# Add more icon resources...
svg = 'C:\path\to\ppic.exe,-2'
gif = 'C:\path\to\ppic.exe,-3'
jpg = 'C:\path\to\ppic.exe,-4'
webp = 'C:\path\to\ppic.exe,-5'
avif = 'C:\path\to\ppic.exe,-6'
others = 'C:\path\to\ppic.exe,-61'
gif = 'C:\path\to\ppic\icons\gif.ico'
jpeg = 'C:\path\to\ppic\icons\jpeg.ico'
png = 'C:\path\to\ppic\icons\png.ico'
psd = 'C:\path\to\ppic\icons\psd.ico'
svg = 'C:\path\to\ppic\icons\svg.ico'
# ======== Behaviors Collection ========
@@ -161,76 +171,191 @@ generic = '"C:\path\to\ppic.exe" "%1"'
[exts.jpg]
# First, we specify its name shown in Windows Explorer.
# The value of this field is the token declared above in "strs" list.
name = "jpg"
name = "jpeg"
# Then, speficy its associated icon.
# The value of this field is the token declared above in "icons" list.
icon = "jpg"
icon = "jpeg"
# At last, specify the behavior of this extension define decide how we open this file.
# The value of this field is the token declared above in "behaviors" list.
behavior = "generic"
# And more file extensions...
[exts.jfif]
name = "jfif"
icon = "jpg"
[exts.jpeg]
name = "jpeg"
icon = "jpeg"
behavior = "generic"
[exts.jfif]
name = "jpeg"
icon = "jpeg"
behavior = "generic"
[exts.gif]
name = "gif"
icon = "gif"
behavior = "generic"
[exts.bmp]
name = "bmp"
icon = "others"
behavior = "generic"
[exts.png]
name = "png"
icon = "png"
behavior = "generic"
[exts.ico]
name = "ico"
icon = "others"
behavior = "generic"
[exts.jpeg]
name = "jpeg"
icon = "jpg"
behavior = "generic"
[exts.tif]
name = "tif"
icon = "others"
behavior = "generic"
[exts.tiff]
name = "tiff"
icon = "others"
behavior = "generic"
[exts.webp]
name = "webp"
icon = "webp"
behavior = "generic"
[exts.svg]
name = "svg"
icon = "svg"
behavior = "generic"
[exts.kra]
name = "kra"
icon = "others"
behavior = "generic"
[exts.xcf]
name = "xcf"
icon = "others"
[exts.webp]
name = "webp"
icon = "generic"
behavior = "generic"
[exts.avif]
name = "avif"
icon = "avif"
icon = "generic"
behavior = "generic"
[exts.ico]
name = "ico"
icon = "generic"
behavior = "generic"
[exts.icns]
name = "icns"
icon = "generic"
behavior = "generic"
[exts.psd]
name = "psd"
icon = "psd"
behavior = "generic"
[exts.psb]
name = "psd"
icon = "psd"
behavior = "generic"
[exts.pdd]
name = "psd"
icon = "psd"
behavior = "generic"
[exts.psdt]
name = "psd"
icon = "psd"
behavior = "generic"
[exts.kra]
name = "kra"
icon = "generic"
behavior = "generic"
[exts.xcf]
name = "xcf"
icon = "generic"
behavior = "generic"
[exts.tga]
name = "tga"
icon = "generic"
behavior = "generic"
[exts.tif]
name = "tif"
icon = "generic"
behavior = "generic"
[exts.tiff]
name = "tif"
icon = "generic"
behavior = "generic"
[exts.bmp]
name = "bmp"
icon = "generic"
behavior = "generic"
[exts.wbmp]
name = "wbmp"
icon = "generic"
behavior = "generic"
[exts.dds]
name = "dds"
icon = "generic"
behavior = "generic"
[exts.hdr]
name = "hdr"
icon = "generic"
behavior = "generic"
[exts.iff]
name = "iff"
icon = "generic"
behavior = "generic"
[exts.pic]
name = "pic"
icon = "generic"
behavior = "generic"
[exts.pcx]
name = "pcx"
icon = "generic"
behavior = "generic"
[exts.qoi]
name = "qoi"
icon = "others"
icon = "generic"
behavior = "generic"
[exts.apng]
name = "apng"
icon = "png"
[exts.ras]
name = "ras"
icon = "generic"
behavior = "generic"
[exts.exr]
name = "exr"
icon = "others"
[exts.sun]
name = "ras"
icon = "generic"
behavior = "generic"
[exts.ora]
name = "ora"
icon = "generic"
behavior = "generic"
[exts.ani]
name = "ani"
icon = "generic"
behavior = "generic"
[exts.pfm]
name = "pfm"
icon = "generic"
behavior = "generic"
[exts.phm]
name = "pfm"
icon = "generic"
behavior = "generic"
[exts.rgb]
name = "rgb"
icon = "generic"
behavior = "generic"
[exts.rgba]
name = "rgb"
icon = "generic"
behavior = "generic"
[exts.bw]
name = "rgb"
icon = "generic"
behavior = "generic"
[exts.sgi]
name = "rgb"
icon = "generic"
behavior = "generic"
[exts.pxr]
name = "pxr"
icon = "generic"
behavior = "generic"
[exts.sct]
name = "sct"
icon = "generic"
behavior = "generic"
-3
View File
@@ -1,3 +0,0 @@
# Pineapple Picture Association
TODO
+36
View File
@@ -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()
+154
View File
@@ -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.
+101
View File
@@ -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()
+19
View File
@@ -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_
+58
View File
@@ -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_
+46
View File
@@ -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_
+20
View File
@@ -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_
-405
View File
@@ -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
-10
View File
@@ -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
-51
View File
@@ -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
-9
View File
@@ -1,9 +0,0 @@
LIBRARY wfassoc
EXPORTS
WFInstallApplicationW
WFInstallApplicationA
WFUninstallApplicationW
WFUninstallApplicationA
WFGenerateProgIDW
WFGenerateProgIDA
-165
View File
@@ -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>
-44
View File
@@ -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>
-407
View File
@@ -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(&regpathAppPaths, 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(&regpathApplicationsRealName, 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(&regpathApplicationsProgId, 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))
);
}
-60
View File
@@ -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
View File
@@ -1 +0,0 @@
#include "wfassoc_private.h"
-9
View File
@@ -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
-256
View File
@@ -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
-136
View File
@@ -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
-81
View File
@@ -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>
+2 -3
View File
@@ -12,6 +12,5 @@ crate-type = ["cdylib"]
[dependencies]
thiserror = { workspace = true }
wfassoc = { path="../wfassoc" }
[build-dependencies]
cbindgen = "0.29.0"
slotmap = "1.1.1"
num_enum = "0.7.6"
+3 -1
View File
@@ -1,5 +1,7 @@
# Wfassoc C/C++ Library
# WFAssoc C/C++ Binding
For how to utilize this library, please see our example located in `example/ppic`. It is a Qt project built with CMake and demonstrate basically all usage of exposed functions in this dynamic library.
All crucial spots are written in this example as the code comment so I don't write them in there again.
Due to the limitation of Rust compiler, you may need to enable `RUSTC_BOOTSTRAP=1` environment when creating binding (it is not necessary for just building this dynamic library). See [this GitHub issue](https://github.com/mozilla/cbindgen/issues/1015) for more info.
+101
View File
@@ -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()
+693
View File
@@ -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_
+595
View File
@@ -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_
+101
View File
@@ -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
+77
View File
@@ -0,0 +1,77 @@
//! The module including all FFI types used by this crate, except string type.
//! For string type, see also [crate::cstr_ffi].
use std::ffi::{c_void, c_char};
use num_enum::TryFromPrimitive;
// region: Misc Types and Constants
/// The type representing the raw pointer to immutable C-style NUL-terminated string.
pub type CStyleString = *const c_char;
/// The invalid value of index.
pub const INVALID_INDEX: usize = usize::MAX;
// endregion
// region: HICON
/// The type representing Win32 HICON handle.
///
/// In theory, we can fetch HICON type from "windows_sys" crate.
/// However, I don't want to add it as this crate's dependency,
/// because I don't use anything within it except this type.
/// So I check Microsoft document, re-define it in there for this crate.
/// Reference: <https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types>
pub type HICON = *mut c_void;
/// The invalid value of Win32 HICON handle.
///
/// The same reason like [WFHICON] to re-define it in there.
pub const INVALID_HICON: HICON = std::ptr::null_mut();
// endregion
// region: Scope
/// The FFI wrapper for [wfassoc::Scope].
#[repr(u32)]
#[derive(Debug, Copy, Clone, TryFromPrimitive)]
pub enum Scope {
User = 0,
System = 1,
}
impl From<Scope> for wfassoc::Scope {
fn from(value: Scope) -> Self {
match value {
Scope::User => wfassoc::Scope::User,
Scope::System => wfassoc::Scope::System,
}
}
}
// endregion
// region: View
/// The FFI wrapper for [wfassoc::View].
#[repr(u32)]
#[derive(Debug, Copy, Clone, TryFromPrimitive)]
pub enum View {
User = 0,
System = 1,
Hybrid = 2,
}
impl From<View> for wfassoc::View {
fn from(value: View) -> Self {
match value {
View::User => wfassoc::View::User,
View::System => wfassoc::View::System,
View::Hybrid => wfassoc::View::Hybrid,
}
}
}
// endregion
+757 -20
View File
@@ -1,24 +1,41 @@
use std::cell::RefCell;
use std::ffi::{CString, c_char};
use thiserror::Error as TeError;
mod cstr_ffi;
mod ffi_types;
mod last_error;
mod string_cache;
mod wrapper;
mod object_pool;
use object_pool::ObjectPool;
use std::sync::{LazyLock, RwLock};
use thiserror::Error as TeError;
use wfassoc::highlevel::{Program, Schema};
// region: Error
/// Error occurs in this crate.
#[derive(Debug, TeError)]
enum Error {
/// Error occurs in wfassoc core.
#[error("{0}")]
Core(#[from] wfassoc::Error),
/// Error when operating Schema.
#[error("wfassoc error: {0}")]
Schema(#[from] wfassoc::highlevel::SchemaError),
/// Error when parsing Schema into Program.
#[error("wfassoc error: {0}")]
ParseProgram(#[from] wfassoc::highlevel::ParseProgramError),
/// Error when operating Program.
#[error("wfassoc error: {0}")]
Program(#[from] wfassoc::highlevel::ProgramError),
#[error("error occurs when parsing into C/C++ string")]
CastIntoCStr(#[from] std::ffi::NulError),
#[error("error occurs when parsing from C/C++ string")]
CastFromCStr(#[from] std::ffi::IntoStringError),
/// Error when manipulating with C-style string.
#[error("C-Style string FFI error: {0}")]
CStrFfi(#[from] cstr_ffi::Error),
/// Error when manipulating with object pool.
#[error("object pool error: {0}")]
ObjectPool(#[from] object_pool::Error),
/// Error occurs when checking enum value
#[error("the enumeration value provided to FFI function is out of its range")]
EnumOutOfRange,
/// Error when manipulating with poison RwLock
#[error("concurrency error: RwLock is poisonous")]
PoisonRwLock,
}
/// Result type used in this crate.
@@ -28,31 +45,751 @@ type Result<T> = std::result::Result<T, Error>;
// region: Macros
macro_rules! in_param_ty {
($t:ty) => {
$t
};
}
macro_rules! out_param_ty {
($t:ty) => {
*mut $t
};
}
macro_rules! set_out_param {
($lhs:expr, $rhs:expr) => {
unsafe { *$lhs = $rhs };
};
}
macro_rules! resolve_enum {
($t:ty, $v:expr) => {
<$t>::try_from($v).map_err(|_| Error::EnumOutOfRange)
};
}
macro_rules! pull_reader {
($pool:expr) => {
$pool.read().map_err(|_| Error::PoisonRwLock)
};
}
macro_rules! pull_writer {
($pool:expr) => {
$pool.write().map_err(|_| Error::PoisonRwLock)
};
}
/// Macro to wrap inner function execution with standard error handling pattern.
///
/// For functions with no output parameter and no input parameters:
/// ```ignore
/// cffi_wrapper!(|| {
/// // inner function body returning Result<()>
/// });
/// ```
///
/// For functions with no output parameter and with input parameters:
/// ```ignore
/// cffi_wrapper!(|param1: Type1, param2: Type2| {
/// // inner function body using param1, param2 returning Result<()>
/// });
/// ```
///
/// For functions with one output parameter and no input parameters:
/// ```ignore
/// cffi_wrapper!(|| -> (out_param, OutType) {
/// // inner function body returning Result<T>
/// });
/// ```
///
/// For functions with one output parameter and with input parameters:
/// ```ignore
/// cffi_wrapper!(|param1: Type1| -> (out_param, OutType) {
/// // inner function body using param1 returning Result<T>
/// });
/// ```
macro_rules! cffi_wrapper {
// Case with output parameter and input parameters
(|$($param:ident: $param_ty:ty),*| -> ($out_param:ident: $out_param_ty:ty) $inner_body:block) => {{
fn inner($($param: $param_ty),*) -> Result<$out_param_ty> $inner_body
match inner($($param),*) {
Ok(rv) => {
set_out_param!($out_param, rv);
last_error::clear_last_error();
true
}
Err(e) => {
last_error::set_last_error(e.to_string().as_str());
false
}
}
}};
// Case with output parameter and no input parameters
(|| -> ($out_param:ident: $out_param_ty:ty) $inner_body:block) => {{
fn inner() -> Result<$out_param_ty> $inner_body
match inner() {
Ok(rv) => {
set_out_param!($out_param, rv);
last_error::clear_last_error();
true
}
Err(e) => {
last_error::set_last_error(e.to_string().as_str());
false
}
}
}};
// Case without output parameter but with input parameters
(|$($param:ident: $param_ty:ty),*| $inner_body:block) => {{
fn inner($($param: $param_ty),*) -> Result<()> $inner_body
match inner($($param),*) {
Ok(_) => {
last_error::clear_last_error();
true
}
Err(e) => {
last_error::set_last_error(e.to_string().as_str());
false
}
}
}};
// Case without output parameter and no input parameters
(|| $inner_body:block) => {{
fn inner() -> Result<()> $inner_body
match inner() {
Ok(_) => {
last_error::clear_last_error();
true
}
Err(e) => {
last_error::set_last_error(e.to_string().as_str());
false
}
}
}};
}
// endregion
// region: Object Pools
static SCHEMA_POOL: LazyLock<RwLock<ObjectPool<Schema>>> =
LazyLock::new(|| RwLock::new(ObjectPool::new()));
static PROGRAM_POOL: LazyLock<RwLock<ObjectPool<Program>>> =
LazyLock::new(|| RwLock::new(ObjectPool::new()));
static EXT_STATUS_POOL: LazyLock<RwLock<ObjectPool<wfassoc::highlevel::ProgramExtStatus>>> =
LazyLock::new(|| RwLock::new(ObjectPool::new()));
static SELF_EXT_STATUS_POOL: LazyLock<
RwLock<ObjectPool<wfassoc::highlevel::ProgramSelfExtStatus>>,
> = LazyLock::new(|| RwLock::new(ObjectPool::new()));
static ICON_RC_POOL: LazyLock<RwLock<ObjectPool<wfassoc::win32::concept::IconRc>>> =
LazyLock::new(|| RwLock::new(ObjectPool::new()));
// endregion
// region: Exposed Types
pub use ffi_types::{
CStyleString as WFCString, HICON as WFHICON, Scope as WFScope, View as WFView,
};
pub use ffi_types::{INVALID_HICON as WF_INVALID_HICON, INVALID_INDEX as WF_INVALID_INDEX};
pub use object_pool::Token as WFToken;
// endregion
// region: Exposed Functions
// region: Facilities
#[unsafe(no_mangle)]
pub extern "C" fn WFStartup() -> bool {
false
// Initialize all pool by fetching writer from them
cffi_wrapper!(|| {
let _pool = pull_writer!(SCHEMA_POOL)?;
let _pool = pull_writer!(PROGRAM_POOL)?;
let _pool = pull_writer!(EXT_STATUS_POOL)?;
let _pool = pull_writer!(SELF_EXT_STATUS_POOL)?;
let _pool = pull_writer!(ICON_RC_POOL)?;
Ok(())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFShutdown() -> bool {
false
// Free all pool stored objects
cffi_wrapper!(|| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
pool.clear();
let mut pool = pull_writer!(PROGRAM_POOL)?;
pool.clear();
let mut pool = pull_writer!(EXT_STATUS_POOL)?;
pool.clear();
let mut pool = pull_writer!(SELF_EXT_STATUS_POOL)?;
pool.clear();
let mut pool = pull_writer!(ICON_RC_POOL)?;
pool.clear();
Ok(())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFGetLastError() -> *const c_char {
pub extern "C" fn WFGetLastError() -> WFCString {
last_error::get_last_error()
}
#[unsafe(no_mangle)]
pub extern "C" fn WFHasPrivilege() -> bool {
wfassoc::utilities::has_privilege()
wfassoc::win32::utilities::has_privilege()
}
#[unsafe(no_mangle)]
pub extern "C" fn WFAdd(left: u32, right: u32, rv: *mut u32) -> bool {
unsafe { *rv = left + right; }
return true;
pub extern "C" fn WFInvalidToken() -> WFToken {
object_pool::invalid_token()
}
// endregion
// region: Schema
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaCreate(out_schema: out_param_ty!(WFToken)) -> bool {
cffi_wrapper!(|| -> (out_schema: WFToken) {
let mut pool = pull_writer!(SCHEMA_POOL)?;
Ok(pool.allocate(Schema::new())?)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaDestroy(in_schema: in_param_ty!(WFToken)) -> bool {
cffi_wrapper!(|in_schema: WFToken| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
Ok(pool.free(in_schema)?)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaSetIdentifier(
in_schema: in_param_ty!(WFToken),
in_value: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
schema.set_identifier(cstr_ffi::parse_ffi_string(in_value)?);
Ok(())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaSetPath(
in_schema: in_param_ty!(WFToken),
in_value: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
schema.set_path(cstr_ffi::parse_ffi_string(in_value)?);
Ok(())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaSetClsid(
in_schema: in_param_ty!(WFToken),
in_value: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
schema.set_clsid(cstr_ffi::parse_ffi_string(in_value)?);
Ok(())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaSetName(
in_schema: in_param_ty!(WFToken),
in_value: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
let name = if in_value.is_null() {
None
} else {
Some(cstr_ffi::parse_ffi_string(in_value)?)
};
schema.set_name(name);
Ok(())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaSetIcon(
in_schema: in_param_ty!(WFToken),
in_value: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
let icon = if in_value.is_null() {
None
} else {
Some(cstr_ffi::parse_ffi_string(in_value)?)
};
schema.set_icon(icon);
Ok(())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaSetBehavior(
in_schema: in_param_ty!(WFToken),
in_value: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
let behavior = if in_value.is_null() {
None
} else {
Some(cstr_ffi::parse_ffi_string(in_value)?)
};
schema.set_behavior(behavior);
Ok(())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaAddStr(
in_schema: in_param_ty!(WFToken),
in_name: in_param_ty!(WFCString),
in_value: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(
|in_schema: WFToken, in_name: WFCString, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
schema.add_str(
cstr_ffi::parse_ffi_string(in_name)?,
cstr_ffi::parse_ffi_string(in_value)?,
)?;
Ok(())
}
)
}
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaAddIcon(
in_schema: in_param_ty!(WFToken),
in_name: in_param_ty!(WFCString),
in_value: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(
|in_schema: WFToken, in_name: WFCString, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
schema.add_icon(
cstr_ffi::parse_ffi_string(in_name)?,
cstr_ffi::parse_ffi_string(in_value)?,
)?;
Ok(())
}
)
}
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaAddBehavior(
in_schema: in_param_ty!(WFToken),
in_name: in_param_ty!(WFCString),
in_value: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(
|in_schema: WFToken, in_name: WFCString, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
schema.add_behavior(
cstr_ffi::parse_ffi_string(in_name)?,
cstr_ffi::parse_ffi_string(in_value)?,
)?;
Ok(())
}
)
}
#[unsafe(no_mangle)]
pub extern "C" fn WFSchemaAddExt(
in_schema: in_param_ty!(WFToken),
in_ext: in_param_ty!(WFCString),
in_ext_name: in_param_ty!(WFCString),
in_ext_icon: in_param_ty!(WFCString),
in_ext_behavior: in_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_schema: WFToken,
in_ext: WFCString,
in_ext_name: WFCString,
in_ext_icon: WFCString,
in_ext_behavior: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?;
schema.add_ext(
cstr_ffi::parse_ffi_string(in_ext)?,
cstr_ffi::parse_ffi_string(in_ext_name)?,
cstr_ffi::parse_ffi_string(in_ext_icon)?,
cstr_ffi::parse_ffi_string(in_ext_behavior)?,
)?;
Ok(())
})
}
// endregion
// region: Program
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramCreate(
in_schema: in_param_ty!(WFToken),
out_program: out_param_ty!(WFToken),
) -> bool {
cffi_wrapper!(|in_schema: WFToken| -> (out_program: WFToken) {
let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.pop(in_schema)?;
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = Program::new(schema)?;
Ok(pool.allocate(program)?)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramDestroy(in_program: in_param_ty!(WFToken)) -> bool {
cffi_wrapper!(|in_program: WFToken| {
let mut pool = pull_writer!(PROGRAM_POOL)?;
Ok(pool.free(in_program)?)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramResolveName(
in_program: in_param_ty!(WFToken),
out_name: out_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_program: WFToken| -> (out_name: WFCString) {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
let name = program.resolve_name()?;
cstr_ffi::set_ffi_string(&name)?;
Ok(cstr_ffi::get_ffi_string())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramResolveIcon(
in_program: in_param_ty!(WFToken),
out_icon_rc: out_param_ty!(WFToken),
) -> bool {
cffi_wrapper!(|in_program: WFToken| -> (out_icon_rc: WFToken) {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
let icon = program.resolve_icon()?;
let mut pool = pull_writer!(ICON_RC_POOL)?;
Ok(pool.allocate(icon)?)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramExtsLen(
in_program: in_param_ty!(WFToken),
out_len: out_param_ty!(usize),
) -> bool {
cffi_wrapper!(|in_program: WFToken| -> (out_len: usize) {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
Ok(program.exts_len())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramFindExt(
in_program: in_param_ty!(WFToken),
in_body: in_param_ty!(WFCString),
out_index: out_param_ty!(usize),
) -> bool {
cffi_wrapper!(|in_program: WFToken, in_body: WFCString| -> (out_index: usize) {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
let body = cstr_ffi::parse_ffi_string(in_body)?;
let index = match program.find_ext(body) {
Some(index) => index,
None => WF_INVALID_INDEX,
};
Ok(index)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramResolveExt(
in_program: in_param_ty!(WFToken),
in_index: in_param_ty!(usize),
out_self_ext_status: out_param_ty!(WFToken),
) -> bool {
cffi_wrapper!(|in_program: WFToken, in_index: usize| -> (out_self_ext_status: WFToken) {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
let self_ext_status = program.resolve_ext(in_index)?;
let mut pool = pull_writer!(SELF_EXT_STATUS_POOL)?;
let token = pool.allocate(self_ext_status)?;
Ok(token)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramRegister(
in_program: in_param_ty!(WFToken),
in_scope: in_param_ty!(u32),
) -> bool {
cffi_wrapper!(|in_program: WFToken, in_scope: u32| {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
let scope = resolve_enum!(WFScope, in_scope)?;
program.register(scope.into())?;
Ok(())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramUnregister(
in_program: in_param_ty!(WFToken),
in_scope: in_param_ty!(u32),
) -> bool {
cffi_wrapper!(|in_program: WFToken, in_scope: u32| {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
let scope = resolve_enum!(WFScope, in_scope)?;
program.unregister(scope.into())?;
Ok(())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramIsRegistered(
in_program: in_param_ty!(WFToken),
in_scope: in_param_ty!(u32),
out_is_registered: out_param_ty!(bool),
) -> bool {
cffi_wrapper!(|in_program: WFToken, in_scope: u32| -> (out_is_registered: bool) {
let pool = pull_reader!(PROGRAM_POOL)?;
let program = pool.get(in_program)?;
let scope = resolve_enum!(WFScope, in_scope)?;
Ok(program.is_registered(scope.into())?)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramLinkExt(
in_program: in_param_ty!(WFToken),
in_scope: in_param_ty!(u32),
in_index: in_param_ty!(usize),
) -> bool {
cffi_wrapper!(|in_program: WFToken, in_scope: u32, in_index: usize| {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
let scope = resolve_enum!(WFScope, in_scope)?;
program.link_ext(scope.into(), in_index)?;
Ok(())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramUnlinkExt(
in_program: in_param_ty!(WFToken),
in_scope: in_param_ty!(u32),
in_index: in_param_ty!(usize),
) -> bool {
cffi_wrapper!(|in_program: WFToken, in_scope: u32, in_index: usize| {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
let scope = resolve_enum!(WFScope, in_scope)?;
program.unlink_ext(scope.into(), in_index)?;
Ok(())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramQueryExt(
in_program: in_param_ty!(WFToken),
in_view: in_param_ty!(u32),
in_index: in_param_ty!(usize),
out_ext_status: out_param_ty!(WFToken),
) -> bool {
cffi_wrapper!(|in_program: WFToken, in_view: u32, in_index: usize| -> (out_ext_status: WFToken) {
let pool = pull_reader!(PROGRAM_POOL)?;
let program = pool.get(in_program)?;
let view = resolve_enum!(WFView, in_view)?;
let ext_status = program.query_ext(view.into(), in_index)?;
let token = match ext_status {
Some(ext_status) => {
let mut pool = pull_writer!(EXT_STATUS_POOL)?;
pool.allocate(ext_status)?
},
None => object_pool::invalid_token(),
};
Ok(token)
})
}
// endregion
// region: Extension Status
#[unsafe(no_mangle)]
pub extern "C" fn WFExtStatusDestroy(in_ext_status: in_param_ty!(WFToken)) -> bool {
cffi_wrapper!(|in_ext_status: WFToken| {
let mut pool = pull_writer!(EXT_STATUS_POOL)?;
Ok(pool.free(in_ext_status)?)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFExtStatusGetName(
in_ext_status: in_param_ty!(WFToken),
out_name: out_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_ext_status: WFToken| -> (out_name: WFCString) {
let pool = pull_reader!(EXT_STATUS_POOL)?;
let ext_status = pool.get(in_ext_status)?;
cstr_ffi::set_ffi_string(ext_status.get_name())?;
Ok(cstr_ffi::get_ffi_string())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFExtStatusGetIcon(
in_ext_status: in_param_ty!(WFToken),
out_icon: out_param_ty!(WFHICON),
) -> bool {
cffi_wrapper!(|in_ext_status: WFToken| -> (out_icon: WFHICON) {
let pool = pull_reader!(EXT_STATUS_POOL)?;
let ext_status = pool.get(in_ext_status)?;
let icon = ext_status.get_icon();
Ok(icon.get_icon())
})
}
// endregion
// region: Self Extension Status
#[unsafe(no_mangle)]
pub extern "C" fn WFSelfExtStatusDestroy(in_self_ext_status: in_param_ty!(WFToken)) -> bool {
cffi_wrapper!(|in_self_ext_status: WFToken| {
let mut pool = pull_writer!(SELF_EXT_STATUS_POOL)?;
Ok(pool.free(in_self_ext_status)?)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFSelfExtStatusGetName(
in_self_ext_status: in_param_ty!(WFToken),
out_name: out_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_self_ext_status: WFToken| -> (out_name: WFCString) {
let pool = pull_reader!(SELF_EXT_STATUS_POOL)?;
let self_ext_status = pool.get(in_self_ext_status)?;
cstr_ffi::set_ffi_string(self_ext_status.get_name())?;
Ok(cstr_ffi::get_ffi_string())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFSelfExtStatusGetIcon(
in_self_ext_status: in_param_ty!(WFToken),
out_icon: out_param_ty!(WFHICON),
) -> bool {
cffi_wrapper!(|in_self_ext_status: WFToken| -> (out_icon: WFHICON) {
let pool = pull_reader!(SELF_EXT_STATUS_POOL)?;
let self_ext_status = pool.get(in_self_ext_status)?;
let icon = self_ext_status.get_icon();
Ok(icon.get_icon())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFSelfExtStatusGetExt(
in_self_ext_status: in_param_ty!(WFToken),
out_inner: out_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_self_ext_status: WFToken| -> (out_inner: WFCString) {
let pool = pull_reader!(SELF_EXT_STATUS_POOL)?;
let self_ext_status = pool.get(in_self_ext_status)?;
cstr_ffi::set_ffi_string(self_ext_status.get_ext())?;
Ok(cstr_ffi::get_ffi_string())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFSelfExtStatusGetDottedExt(
in_self_ext_status: in_param_ty!(WFToken),
out_inner: out_param_ty!(WFCString),
) -> bool {
cffi_wrapper!(|in_self_ext_status: WFToken| -> (out_inner: WFCString) {
let pool = pull_reader!(SELF_EXT_STATUS_POOL)?;
let self_ext_status = pool.get(in_self_ext_status)?;
cstr_ffi::set_ffi_string(self_ext_status.get_dotted_ext().as_str())?;
Ok(cstr_ffi::get_ffi_string())
})
}
// endregion
// region: Icon Resource
#[unsafe(no_mangle)]
pub extern "C" fn WFIconRcDestroy(in_icon_rc: in_param_ty!(WFToken)) -> bool {
cffi_wrapper!(|in_icon_rc: WFToken| {
let mut pool = pull_writer!(ICON_RC_POOL)?;
Ok(pool.free(in_icon_rc)?)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFIconRcGetIcon(
in_icon_rc: in_param_ty!(WFToken),
out_icon: out_param_ty!(WFHICON),
) -> bool {
cffi_wrapper!(|in_icon_rc: WFToken| -> (out_icon: WFHICON) {
let pool = pull_reader!(ICON_RC_POOL)?;
let icon_rc = pool.get(in_icon_rc)?;
Ok(icon_rc.get_icon())
})
}
// endregion
+95
View File
@@ -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)
}
}
-52
View File
@@ -1,52 +0,0 @@
//! When calling this dynamic library with outside programs,
//! outer programs may usually need to fetch string resource produced by Rust code.
//! However it is impossible pass Rust string directly to outer program.
//!
//! This module provide **thread independent** string cache for resolving this issue.
//! When we need pass string to outer programs, we push that string into this string as C-like format,
//! then return its pointer to outer program.
//! So that outside program can utilize it like calling C/C++ library.
//! The only thing that outer programs should note is that this string is volatile,
//! once they get it, they must dupliate it immediately before any futher calling to this dynamic library.
use std::cell::RefCell;
use std::ffi::{CString, c_char};
struct StringCache {
msg: CString,
}
impl StringCache {
fn new() -> Self {
Self {
msg: CString::new("").expect("empty string must be valid for CString"),
}
}
pub fn set_msg(&mut self, msg: &str) {
self.msg = CString::new(msg).expect("unexpected blank in string push into string cache");
}
pub fn get_msg(&self) -> *const c_char {
self.msg.as_ptr()
}
pub fn clear_msg(&mut self) {
self.msg = CString::new("").expect("empty string must be valid for CString");
}
}
thread_local! {
static STRING_CACHE: RefCell<StringCache> = RefCell::new(StringCache::new());
}
/// Set thread local string cache.
pub fn set_string_cache(msg: &str) {
STRING_CACHE.with(|e| {
e.borrow_mut().set_msg(msg);
});
}
/// Get const pointer to thread local string cache for outside program visiting.
pub fn get_string_cache() -> *const c_char {
STRING_CACHE.with(|e| e.borrow().get_msg())
}
-83
View File
@@ -1,83 +0,0 @@
use std::collections::HashMap;
use thiserror::Error as TeError;
/// Error occurs in this module.
#[derive(Debug, TeError)]
pub enum Error {}
/// Result type used in this module.
type Result<T> = std::result::Result<T, Error>;
/// Schema is the sketchpad of complete Program.
///
/// We will create a Schema first, fill some properties, add file extensions,
/// then convert it into immutable Program for following using.
#[derive(Debug)]
pub struct Schema {
identifier: String,
path: String,
clsid: String,
icons: HashMap<String, String>,
behaviors: HashMap<String, String>,
exts: HashMap<String, SchemaExt>,
}
/// Internal used struct as the Schema file extensions hashmap value type.
#[derive(Debug)]
struct SchemaExt {
name: String,
icon: String,
behavior: String,
}
impl Schema {
pub fn new() -> Self {
Self {
identifier: String::new(),
path: String::new(),
clsid: String::new(),
icons: HashMap::new(),
behaviors: HashMap::new(),
exts: HashMap::new(),
}
}
pub fn set_identifier(&mut self, identifier: &str) -> Result<()> {}
pub fn set_path(&mut self, exe_path: &str) -> Result<()> {}
pub fn set_clsid(&mut self, clsid: &str) -> Result<()> {}
pub fn add_icon(&mut self, name: &str, value: &str) -> Result<()> {}
pub fn add_behavior(&mut self, name: &str, value: &str) -> Result<()> {}
pub fn add_ext(
&mut self,
ext: &str,
ext_name: &str,
ext_icon: &str,
ext_behavior: &str,
) -> Result<()> {
}
pub fn into_program(self) -> Result<Program> {
Program::new(self)
}
}
/// Program is a complete and immutable program representer
pub struct Program {}
impl TryFrom<Schema> for Program {
type Error = Error;
fn try_from(value: Schema) -> std::result::Result<Self, Self::Error> {
Self::new(value)
}
}
impl Program {
pub fn new(schema: Schema) -> Result<Self> {}
}
+1 -1
View File
@@ -1,4 +1,4 @@
# Wfassoc Executable
# WFAssoc Executable
For the syntax of this executable's parameters, execute it with `--help` for more.
+3 -3
View File
@@ -83,8 +83,8 @@ pub enum CliCommands {
#[command(about = "Fetch the status of registration with given manifest and scope.")]
Status {
/// The view where fetch info.
#[arg(short = 't', long = "target", value_name = "TARGET", required = true, value_enum, default_value_t = RegView::User)]
target: RegView,
#[arg(short = 't', long = "target", value_name = "TARGET", required = true, value_enum, default_value_t = RegScope::User)]
target: RegScope,
},
#[command(name = "ext")]
#[command(about = "File extension related operations according to given program manifest.")]
@@ -114,7 +114,7 @@ pub enum CliExtCommands {
/// The scope where unlink file extension.
#[arg(short = 't', long = "target", value_name = "TARGET", required = true, value_enum, default_value_t = RegScope::User)]
target: RegScope,
// The file extensions used for this operation. Specify * for all file extension.
// The file extensions (without leading dot) used for this operation. Specify * for all file extension.
#[arg(required = true, value_name = "EXTS", num_args = 1..)]
exts: Vec<String>,
},
+1 -1
View File
@@ -63,7 +63,7 @@ impl Manifest {
#[derive(Debug, TeError)]
pub enum ParseSchemaError {
/// Error when operating with schema.
#[error("{0}")]
#[error("failed on parsing manifest file to Schema: {0}")]
Schema(#[from] wfassoc::highlevel::SchemaError)
}
-162
View File
@@ -1,162 +0,0 @@
pub(crate) mod cli;
pub(crate) mod manifest;
use clap::Parser;
use cli::{Cli, Commands, Target};
use comfy_table::Table;
use manifest::Manifest;
use std::collections::HashMap;
use std::process;
use thiserror::Error as TeError;
use wfassoc::{Program, Scope, Token, View};
// region: Basic Types
/// All errors occurs in this executable.
#[derive(TeError, Debug)]
enum Error {
/// Error from wfassoc core.
#[error("{0}")]
Core(#[from] wfassoc::Error),
/// Error when parsing manifest TOML file.
#[error("invalid manifest file: {0}")]
Manifest(#[from] manifest::Error),
/// Error when specifying invalid manner name for extension.
#[error("extension {ext} associated manner {manner} is invalid in manifest file")]
InvalidMannerName { manner: String, ext: String },
}
/// Result type used in this executable.
type Result<T> = std::result::Result<T, Error>;
// endregion
// region: Correponding Runner
struct Composition {
program: Program,
ext_tokens: Vec<Token>,
}
impl Composition {
pub fn new(cli: &Cli) -> Result<Composition> {
// Open file and read manifest TOML file
let mf = Manifest::from_file(&cli.config_file)?;
// Create instance
let mut program = Program::new(&mf.identifier, &cli.config_file)?;
// Setup manner
let mut manner_token_map: HashMap<&str, Token> = HashMap::new();
for (k, v) in mf.manners.iter() {
let token = program.add_manner(v.as_str())?;
manner_token_map.insert(k.as_str(), token);
}
// Setup extension
let mut ext_tokens = Vec::new();
for (k, v) in mf.exts.iter() {
let token = match manner_token_map.get(v.as_str()) {
Some(v) => v,
None => {
return Err(Error::InvalidMannerName {
manner: v.to_string(),
ext: k.clone(),
});
}
};
let token = program.add_ext(k.as_str(), *token)?;
ext_tokens.push(token);
}
// Okey
Ok(Self {
program,
ext_tokens,
})
}
pub fn get_program(&self) -> &Program {
&self.program
}
pub fn iter_ext_tokens(&self) -> impl Iterator<Item = Token> {
self.ext_tokens.iter().copied()
}
}
fn run_register(cli: &Cli, target: &Target) -> Result<()> {
let composition = Composition::new(cli)?;
let scope = target.clone().into();
// Register program
let program = composition.get_program();
program.register(scope)?;
// Link all file extensions
for token in composition.iter_ext_tokens() {
program.link_ext(token, scope)?;
}
println!("Register OK.");
Ok(())
}
fn run_unregister(cli: &Cli, target: &Target) -> Result<()> {
let composition = Composition::new(cli)?;
let scope = target.clone().into();
// Unlink all file extensions
let program = composition.get_program();
for token in composition.iter_ext_tokens() {
program.unlink_ext(token, scope)?;
}
// Unregister prorgam
program.unregister(scope)?;
println!("Unregister OK.");
Ok(())
}
fn run_query(cli: &Cli) -> Result<()> {
let composition = Composition::new(cli)?;
// Show file association
let mut table = Table::new();
table.set_header(["Extension", "Hybrid", "User", "System"]);
let program = composition.get_program();
for token in composition.iter_ext_tokens() {
let cell_ext = program.get_ext_str(token).unwrap();
let cell_hybrid = program
.query_ext(token, View::Hybrid)?
.map(|pi| pi.to_string())
.unwrap_or_default();
let cell_user = program
.query_ext(token, View::User)?
.map(|pi| pi.to_string())
.unwrap_or_default();
let cell_system = program
.query_ext(token, View::System)?
.map(|pi| pi.to_string())
.unwrap_or_default();
table.add_row([cell_ext, cell_hybrid, cell_user, cell_system]);
}
println!("{table}");
Ok(())
}
// endregion
fn main() {
let cli = Cli::parse();
let rv = match &cli.command {
Commands::Register { target } => run_register(&cli, target),
Commands::Unregister { target } => run_unregister(&cli, target),
Commands::Query => run_query(&cli),
};
rv.unwrap_or_else(|e| {
eprintln!("Runtime error: {}.", e);
process::exit(1)
});
}
+107 -24
View File
@@ -1,7 +1,10 @@
use crate::cli;
use crate::manifest;
use comfy_table::Table;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use thiserror::Error as TeError;
use toml;
// region: Error Handling
@@ -9,17 +12,31 @@ use thiserror::Error as TeError;
#[derive(Debug, TeError)]
pub enum Error {
/// Error when parsing Manifest TOML file.
#[error("{0}")]
#[error("initialization error: {0}")]
ParseManifest(#[from] manifest::ParseManifestError),
/// Error when parsing Manifest into Schema.
#[error("{0}")]
#[error("initialization error: {0}")]
ParseSchema(#[from] manifest::ParseSchemaError),
/// Error when serializing TOML
#[error("initialization error: {0}")]
SerializeToml(#[from] toml::ser::Error),
/// Error when parsing Schema into Program.
#[error("{0}")]
#[error("wfassoc error: {0}")]
ParseProgram(#[from] wfassoc::highlevel::ParseProgramError),
/// Error when operating Program.
#[error("{0}")]
#[error("wfassoc error: {0}")]
Program(#[from] wfassoc::highlevel::ProgramError),
/// Find duplicated name when converting extension name to index
#[error("given extension name {0} has been specified more than one time")]
DupExtName(String),
/// Find invalid name when converting extension name to index
#[error("given extension name {0} is not presented in application")]
BadExtName(String),
/// Find star (*) extension name with other extension names when converting extension name to index
#[error(r#"wildcard extension name "*" is not allowed to be used with other extension names"#)]
ExclusiveStarExtName,
}
/// Result type used in this module.
@@ -27,10 +44,38 @@ type Result<T> = std::result::Result<T, Error>;
// endregion
// region: Utilities Functions
// region: Utilities
fn stringified_exts_to_indices(program: &wfassoc::Program, exts: Vec<String>) -> Result<Vec<usize>> {
todo!()
fn stringified_exts_to_indices(
program: &wfassoc::Program,
exts: Vec<String>,
) -> Result<Vec<usize>> {
// Check for duplicate extension names
let mut seen = HashSet::new();
for ext in &exts {
if !seen.insert(ext.as_str()) {
return Err(Error::DupExtName(ext.clone()));
}
}
// Check for star (*) with other extensions
let has_star = exts.iter().any(|ext| ext == "*");
if has_star && exts.len() > 1 {
return Err(Error::ExclusiveStarExtName);
}
// If star is present alone, return fixed list from zero to the maximum ext index.
if has_star {
return Ok((0..program.exts_len()).collect());
}
// Convert each extension name to index using program.find_ext()
let indices = exts
.into_iter()
.map(|ext| program.find_ext(&ext).ok_or(Error::BadExtName(ext)))
.collect::<Result<Vec<_>>>()?;
Ok(indices)
}
// endregion
@@ -38,33 +83,29 @@ fn stringified_exts_to_indices(program: &wfassoc::Program, exts: Vec<String>) ->
// region: Respective Runners
fn run_register(mut program: wfassoc::Program, scope: wfassoc::Scope) -> Result<()> {
Ok(program.register(scope)?)
program.register(scope)?;
println!("Application now is installed.");
Ok(())
}
fn run_unregister(mut program: wfassoc::Program, scope: wfassoc::Scope) -> Result<()> {
Ok(program.unregister(scope)?)
program.unregister(scope)?;
println!("Application now is uninstalled.");
Ok(())
}
fn run_status(program: wfassoc::Program, view: wfassoc::View) -> Result<()> {
if program.is_registered(view)? {
fn run_status(program: wfassoc::Program, scope: wfassoc::Scope) -> Result<()> {
if program.is_registered(scope)? {
println!("Application is installed.");
} else {
println!("Application is not installed.");
}
Ok(())
}
fn run_ext_link(program: wfassoc::Program, scope: wfassoc::Scope, exts: Vec<String>) -> Result<()> {
let exts = stringified_exts_to_indices(&program, exts)?;
for index in exts {
program.link_ext(scope, index)?;
}
Ok(())
}
fn run_ext_unlink(
program: wfassoc::Program,
fn run_ext_link(
mut program: wfassoc::Program,
scope: wfassoc::Scope,
exts: Vec<String>,
) -> Result<()> {
@@ -72,6 +113,22 @@ fn run_ext_unlink(
for index in exts {
program.link_ext(scope, index)?;
}
println!("File extension now is linked.");
Ok(())
}
fn run_ext_unlink(
mut program: wfassoc::Program,
scope: wfassoc::Scope,
exts: Vec<String>,
) -> Result<()> {
let exts = stringified_exts_to_indices(&program, exts)?;
for index in exts {
program.unlink_ext(scope, index)?;
}
println!("File extension now is unlinked.");
Ok(())
}
@@ -80,7 +137,34 @@ fn run_ext_list(
view: wfassoc::View,
style: cli::ExtListStyle,
) -> Result<()> {
todo!()
// Fetch info
let mut ext_list: HashMap<String, Option<String>> = HashMap::new();
for index in 0..program.exts_len() {
let self_ext_status = program.resolve_ext(index)?;
let status = program.query_ext(view, index)?;
ext_list.insert(
self_ext_status.get_dotted_ext(),
status.map(|s| s.get_name().to_string()),
);
}
// Output by styles
use cli::ExtListStyle;
match style {
ExtListStyle::Human => {
let mut table = Table::new();
table.set_header(["Extension", "Association"]);
for (k, v) in ext_list {
table.add_row([k, v.unwrap_or("".to_string())]);
}
println!("{table}");
}
ExtListStyle::Machine => {
let stoml = toml::to_string(&ext_list)?;
println!("{stoml}")
}
}
Ok(())
}
// endregion
@@ -88,7 +172,6 @@ fn run_ext_list(
pub fn run(c: cli::Cli) -> Result<()> {
// Read manifest file first
let mf = manifest::Manifest::from_file(Path::new(&c.manifest_file))?;
println!("{:?}", mf);
// Parse it into schema
let schema = mf.into_schema()?;
// Parse it into program
+3
View File
@@ -22,3 +22,6 @@ widestring = "1.2.1"
indexmap = "2.11.4"
regex = "1.11.3"
uuid = { version = "1.18.1", features = ["v4"] }
[dev-dependencies]
strfmt = "0.2.5"
+21 -1
View File
@@ -1,4 +1,6 @@
# WFassoc Core
# WFAssoc Core
## Usage
This crate provides low level API and high level API for manipulating Windows file associations at the same time.
For the convenient use of this project, the root module of this crate re-expose high level API.
@@ -10,3 +12,21 @@ Oppositely, for visiting low level API, please use `lowlevel` module.
If you are a programmer who want to take a deep into the internal implementations,
see `win32` module and its submodules for detail.
## Test Notes
Some tests of this crate may be dangerous because they need to manipulate Windows Registry.
So it is highly recommend that run these tests in sandbox environment.
In detailed words, you should run `cargo test --no-run` to build all test first,
then fetch the path to executable tests according to this command shown on console.
Then execute these executable tests in your sandbox for testing this crate.
Additionally, some tests also need Administration permission for testing,
because it requires write permission in HKLM.
If you do not test it with sandbox and administrative environment,
test program will assert paniked and tell you how to resolve these issues.
The reason why do not run `cargo test` in sandbox environment directly,
is that `cargo` can not find built tests located in host machine.
It will try to fetch all dependencies again and rebuild test in sandbox entirely.
So we use this complex way for testing.
-391
View File
@@ -1,391 +0,0 @@
//! The module including all struct representing Windows file association concept,
//! like file extension, ProgId, CLSID and etc.
use crate::extra::windows::{Ext, ProgId};
use crate::extra::winreg as winreg_extra;
use crate::utilities;
use std::fmt::Display;
use std::str::FromStr;
use thiserror::Error as TeError;
use winreg::RegKey;
use winreg::enums::{
HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE,
};
// region: Error Types
/// All possible error occurs in this crate.
#[derive(Debug, TeError)]
pub enum Error {
#[error("error occurs when manipulating with Registry: {0}")]
BadRegOper(#[from] std::io::Error),
#[error("{0}")]
ParseExt(#[from] crate::extra::windows::ParseExtError),
#[error("{0}")]
ParseProgId(#[from] crate::extra::windows::ParseProgIdError),
#[error("{0}")]
ParseProgIdKind(#[from] ParseProgIdKindError),
#[error("{0}")]
BlankPath(#[from] crate::extra::winreg::BlankPathError),
}
/// The result type used in this crate.
pub type Result<T> = std::result::Result<T, Error>;
// endregion
// region: Data Types
/// The token for access registered items in Program.
/// This is usually returned when you registering them.
pub type Token = usize;
// region: Scope
/// The scope where wfassoc will register and unregister application.
#[derive(Debug, Copy, Clone)]
pub enum Scope {
/// Scope for current user.
User,
/// Scope for all users under this computer.
System,
}
/// The error occurs when cast View into Scope.
#[derive(Debug, TeError)]
#[error("hybrid View can not be cast into Scope")]
pub struct TryFromViewError {}
impl TryFromViewError {
fn new() -> Self {
Self {}
}
}
impl TryFrom<View> for Scope {
type Error = TryFromViewError;
fn try_from(value: View) -> std::result::Result<Self, Self::Error> {
match value {
View::User => Ok(Self::User),
View::System => Ok(Self::System),
View::Hybrid => Err(TryFromViewError::new()),
}
}
}
impl Scope {
/// Check whether we have enough privilege when operating in current scope.
/// If we have, return true, otherwise false.
pub fn has_privilege(&self) -> bool {
// If we operate on System, and we do not has privilege,
// we think we do not have privilege, otherwise,
// there is no privilege required.
!matches!(self, Self::System if !utilities::has_privilege())
}
}
// endregion
// region: View
/// The view when wfassoc querying file extension association.
#[derive(Debug, Copy, Clone)]
pub enum View {
/// The view of current user.
User,
/// The view of system.
System,
/// Hybrid view of User and System.
/// It can be seen as that we use System first and then use User to override any existing items.
Hybrid,
}
impl From<Scope> for View {
fn from(value: Scope) -> Self {
match value {
Scope::User => Self::User,
Scope::System => Self::System,
}
}
}
// endregion
// region: ProgId Kind
/// The error occurs when parsing ProgId kind.
#[derive(Debug, TeError)]
pub enum ParseProgIdKindError {
#[error("{0}")]
FromStd(#[from] crate::extra::windows::ParseProgIdError),
#[error("given ProgId is blank")]
BlankProgId,
}
/// The variant of ProgId for the compatibility
/// with those software which do not follow Microsoft suggestions.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ProgIdKind {
/// Other ProgId which not follow Microsoft standards.
Other(String),
/// Standard ProgId.
Std(ProgId),
}
impl Display for ProgIdKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProgIdKind::Other(v) => write!(f, "{}", v),
ProgIdKind::Std(prog_id) => write!(f, "{}", prog_id),
}
}
}
impl FromStr for ProgIdKind {
type Err = ParseProgIdKindError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
if s.is_empty() {
Err(ParseProgIdKindError::BlankProgId)
} else {
Ok(match s.parse::<ProgId>() {
Ok(v) => Self::Std(v),
Err(_) => Self::Other(s.to_string()),
})
}
}
}
// endregion
// endregion
// region: Registry Visitor
// region: Application Visitor
/// The static struct for visiting "Application" in registry
pub struct ApplicationVisitor {}
impl ApplicationVisitor {
const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths";
const APPLICATIONS: &str = "Software\\Classes\\Applications";
/// Open a readonly registry key to "App Paths" with given scope.
pub fn open_app_paths(scope: Scope) -> Result<RegKey> {
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let app_paths = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?;
Ok(app_paths)
}
/// Open a readonly registry key to "Applications" with given scope.
pub fn open_applications(scope: Scope) -> Result<RegKey> {
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let applications = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
Ok(applications)
}
}
// endregion
// region: Classes Visitor
/// The static struct for visiting "Classes" in registry.
pub struct ClassesVisitor {}
impl ClassesVisitor {
const CLASSES: &str = "Software\\Classes";
/// Open a readonly registry key to "Classes" with given view.
pub fn open_with_view(view: View) -> Result<RegKey> {
// Fetch root key and navigate to Classes
let hk = match view {
View::User => RegKey::predef(HKEY_CURRENT_USER),
View::System => RegKey::predef(HKEY_LOCAL_MACHINE),
View::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
};
let classes = match view {
View::User | View::System => hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?,
View::Hybrid => hk.open_subkey_with_flags("", KEY_READ)?,
};
Ok(classes)
}
/// Open a readonly registry key to "Classes" with given scope.
pub fn open_with_scope(scope: Scope) -> Result<RegKey> {
// Fetch root key and navigate to Classes
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let classes = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
Ok(classes)
}
}
// endregion
// endregion
// region: File Extension Registry Key
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ExtKey {
ext: Ext,
}
impl ExtKey {
/// Create new file extension registry key representer.
pub fn new(s: &str) -> Result<Self> {
Ok(Self {
ext: Ext::from_str(s)?,
})
}
/// Fetch the reference to inner extension representer.
pub fn as_inner(&self) -> &Ext {
&self.ext
}
}
impl ExtKey {
/// Set the default "Open With" of this file extension to given ProgId.
pub fn link(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> {
use winreg_extra::blank_path_guard;
// Open Classes key
let classes = ClassesVisitor::open_with_scope(scope)?;
// Open or create this extension key
let (subkey, _) =
classes.create_subkey_with_flags(blank_path_guard(self.ext.to_string())?, KEY_WRITE)?;
// Set the default way to open this file extension
subkey.set_value("", &prog_id.as_inner().to_string())?;
// Okey
Ok(())
}
/// Reset the default "Open With" of this file extension to blank.
///
/// If the default "Open With" of this file extension is not given ProgId,
/// or there is no such file extension, this function do nothing.
pub fn unlink(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> {
use winreg_extra::{blank_path_guard, try_get_value, try_open_subkey_with_flags};
// Open Classes key
let classes = ClassesVisitor::open_with_scope(scope)?;
// Open key for this extension.
// If there is no such key, return directly.
if let Some(subkey) = try_open_subkey_with_flags(
&classes,
blank_path_guard(self.ext.to_string())?,
KEY_WRITE,
)? {
// Only delete the default key if it is equal to our ProgId
if let Some(value) = try_get_value::<String, _>(&subkey, "")? {
if value == prog_id.as_inner().to_string() {
// Delete the default key.
subkey.delete_value("")?;
}
}
}
// Okey
Ok(())
}
/// Query the default "Open With" of this file extension associated ProgId.
///
/// This function will return its associated ProgId if "Open With" was set.
pub fn query(&self, view: View) -> Result<Option<ProgIdKey>> {
use winreg_extra::{blank_path_guard, try_get_value, try_open_subkey_with_flags};
// Open Classes key
let classes = ClassesVisitor::open_with_view(view)?;
// Open key for this extension if possible
let rv = match try_open_subkey_with_flags(
&classes,
blank_path_guard(self.ext.to_string())?,
KEY_READ,
)? {
Some(subkey) => {
// Try get associated ProgId if possible
match try_get_value::<String, _>(&subkey, "")? {
Some(value) => Some(ProgIdKey::new(value.as_str())?),
None => None,
}
}
None => None,
};
// Okey
Ok(rv)
}
}
// endregion
// region: ProgId Registry Key
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ProgIdKey {
prog_id: ProgIdKind,
}
impl ProgIdKey {
/// Create new ProgId registry representer.
pub fn new(s: &str) -> Result<Self> {
Ok(Self {
prog_id: ProgIdKind::from_str(s)?,
})
}
/// Fetch the reference to inner ProgId.
pub fn as_inner(&self) -> &ProgIdKind {
&self.prog_id
}
}
impl ProgIdKey {
/// Create ProgId into Registry in given scope with given parameters
pub fn create(&self, scope: Scope, command: &str) -> Result<()> {
use winreg_extra::blank_path_guard;
let classes = ClassesVisitor::open_with_scope(scope)?;
let (subkey, _) = classes
.create_subkey_with_flags(blank_path_guard(self.prog_id.to_string())?, KEY_WRITE)?;
// Create verb
let (subkey_verb, _) = subkey.create_subkey_with_flags("open", KEY_READ)?;
let (subkey_command, _) = subkey_verb.create_subkey_with_flags("command", KEY_WRITE)?;
subkey_command.set_value("", &command.to_string())?;
Ok(())
}
/// Delete this ProgId from registry in given scope.
pub fn delete(&self, scope: Scope) -> Result<()> {
use winreg_extra::blank_path_guard;
let classes = ClassesVisitor::open_with_scope(scope)?;
classes.delete_subkey_all(blank_path_guard(self.prog_id.to_string())?)?;
Ok(())
}
}
// endregion
+8 -524
View File
@@ -1,56 +1,6 @@
use crate::{lowlevel, utilities, win32::{self, concept}};
use regex::Regex;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::path::Path;
use std::rc::Rc;
use std::sync::LazyLock;
use thiserror::Error as TeError;
//! The module gives a convenient highlevel wrapper for lowlevel file association operations.
pub use lowlevel::{Scope, View};
// region: Error Type
/// Error occurs when operating with [Schema].
#[derive(Debug, TeError)]
pub enum SchemaError {
#[error("duplicate key: {0}")]
DuplicateKey(String),
}
/// Error occurs when trying converting [Schema] into [Program].
#[derive(Debug, TeError)]
pub enum ParseProgramError {
#[error("{0}")]
BadExtBody(#[from] concept::BadExtBodyError),
#[error("{0}")]
BadProgIdPart(#[from] concept::BadProgIdPartError),
#[error("{0}")]
BadFileName(#[from] concept::BadFileNameError),
#[error("{0}")]
ParseCmdLine(#[from] concept::ParseCmdLineError),
#[error("{0}")]
CastOsStr(#[from] utilities::CastOsStrError),
#[error("given path doesn't has legal file name part")]
NoFileNamePart,
#[error("given path doesn't has legal directory part")]
NoDirNamePart,
#[error("given identifier is not presented in dict")]
NoSuchIdentifier,
#[error("extension name should not be empty")]
EmptyExtension,
#[error("given program identifier is not allowed")]
BadIdentifier,
}
/// Error occurs when operating with [Program].
#[derive(Debug, TeError)]
pub enum ProgramError {
#[error("{0}")]
Lowlevel(#[from] lowlevel::Error),
}
// endregion
use crate::lowlevel;
// region: Utilities
@@ -73,479 +23,13 @@ macro_rules! debug_println {
// endregion
// region: Schema
// region: Exposed Stuff
// region: Schema Body
mod schema;
mod program;
/// The sketchpad of complete [Program].
///
/// In suggested usage, we will create a [Schema] first,
/// fill some essential and optional properties,
/// then add file extensions which we need.
/// And finally convert it into immutable [Program] for formal using.
#[derive(Debug)]
pub struct Schema {
identifier: String,
path: String,
clsid: String,
name: Option<String>,
icon: Option<String>,
behavior: Option<String>,
strs: HashMap<String, String>,
icons: HashMap<String, String>,
behaviors: HashMap<String, String>,
exts: HashMap<String, SchemaExt>,
}
impl Schema {
pub fn new() -> Self {
Self {
identifier: String::new(),
path: String::new(),
clsid: String::new(),
name: None,
icon: None,
behavior: None,
strs: HashMap::new(),
icons: HashMap::new(),
behaviors: HashMap::new(),
exts: HashMap::new(),
}
}
/// Set the identifier of the schema.
///
/// The identifier is used to build ProgId.
/// So it should starts with an ASCII letter and followed by zero or more ASCII letters, digits, underline and hyphen.
/// And it should not be empty.
pub fn set_identifier(&mut self, identifier: &str) -> () {
self.identifier = identifier.to_string();
}
/// Set the absolute path to the executable file.
pub fn set_path(&mut self, exe_path: &str) -> () {
self.path = exe_path.to_string();
}
pub fn set_clsid(&mut self, clsid: &str) -> () {
self.clsid = clsid.to_string();
}
pub fn set_name(&mut self, name: Option<&str>) -> () {
self.name = name.map(|n| n.to_string());
}
pub fn set_icon(&mut self, icon: Option<&str>) -> () {
self.icon = icon.map(|i| i.to_string());
}
pub fn set_behavior(&mut self, behavior: Option<&str>) -> () {
self.behavior = behavior.map(|b| b.to_string());
}
pub fn add_str(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
match self.strs.insert(name.to_string(), value.to_string()) {
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
None => Ok(()),
}
}
pub fn add_icon(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
match self.icons.insert(name.to_string(), value.to_string()) {
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
None => Ok(()),
}
}
pub fn add_behavior(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
match self.behaviors.insert(name.to_string(), value.to_string()) {
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
None => Ok(()),
}
}
/// Add a file extension to the schema.
///
/// The parameter `ext` is the file extension without leading dot `.`.
pub fn add_ext(
&mut self,
ext: &str,
ext_name: &str,
ext_icon: &str,
ext_behavior: &str,
) -> Result<(), SchemaError> {
match self.exts.insert(
ext.to_string(),
SchemaExt::new(ext_name, ext_icon, ext_behavior),
) {
Some(_) => Err(SchemaError::DuplicateKey(ext.to_string())),
None => Ok(()),
}
}
/// Try converting [Schema] into [Program].
///
/// This is equivalent to [Program::new].
pub fn into_program(self) -> Result<Program, ParseProgramError> {
Program::new(self)
}
}
// endregion
// region: Schema Internals
/// Internal used struct as the Schema file extensions hashmap value type.
#[derive(Debug)]
struct SchemaExt {
name: String,
icon: String,
behavior: String,
}
impl SchemaExt {
fn new(name: &str, icon: &str, behavior: &str) -> Self {
Self {
name: name.to_string(),
icon: icon.to_string(),
behavior: behavior.to_string(),
}
}
}
// endregion
// endregion
// region: Program
// region: Program Body
/// Program is a complete and immutable program representer
pub struct Program {
app_paths_key: lowlevel::AppPathsKey,
applications_key: lowlevel::ApplicationsKey,
app_path: String,
app_dir_path: String,
name: Option<Rc<ProgramStr>>,
icon: Option<Rc<ProgramIcon>>,
behavior: Option<Rc<ProgramBehavior>>,
strs: Vec<Rc<ProgramStr>>,
icons: Vec<Rc<ProgramIcon>>,
behaviors: Vec<Rc<ProgramBehavior>>,
ext_keys: Vec<ProgramProgIdExtKey>,
}
impl TryFrom<Schema> for Program {
type Error = ParseProgramError;
fn try_from(value: Schema) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl Program {
/// Extract the file name part from full path to application,
/// which was used in Registry path component.
fn extract_file_name(full_path: &Path) -> Result<&OsStr, ParseProgramError> {
full_path
.file_name()
.ok_or(ParseProgramError::NoFileNamePart)
}
/// Extract the start in path from full path to application,
/// which basically is the stem of full path.
fn extract_dir_path(full_path: &Path) -> Result<&OsStr, ParseProgramError> {
full_path
.parent()
.map(|p| p.as_os_str())
.ok_or(ParseProgramError::NoDirNamePart)
}
fn flat_hashmap<V, U, F>(
hashmap: &HashMap<String, V>,
f: F,
) -> Result<(Vec<U>, HashMap<String, usize>), ParseProgramError>
where
F: Fn(&V) -> Result<U, ParseProgramError>,
{
let mut indexmap: HashMap<String, usize> = HashMap::with_capacity(hashmap.len());
let mut vector: Vec<U> = Vec::with_capacity(hashmap.len());
for (key, value) in hashmap.into_iter() {
indexmap.insert(key.clone(), vector.len());
vector.push(f(value)?);
}
Ok((vector, indexmap))
}
fn resolve_index<T>(
key: &String,
vector: &Vec<Rc<T>>,
index_map: &HashMap<String, usize>,
) -> Result<Rc<T>, ParseProgramError> {
match index_map.get(key) {
Some(index) => Ok(vector
.get(*index)
.expect("unexpected invalid index")
.clone()),
None => Err(ParseProgramError::NoSuchIdentifier),
}
}
/// Build ProgId from identifier and given file extension.
fn build_progid(
identifier: &str,
ext: &str,
) -> Result<lowlevel::LosseProgId, ParseProgramError> {
// Use Regex to check identifier
static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^[a-zA-Z][a-zA-Z0-9_-]*$").expect("unexpected bad regex pattern string")
});
let identifier = match RE.captures(identifier) {
Some(_) => identifier,
None => return Err(ParseProgramError::BadIdentifier),
};
// Capitalize first ASCII of ext
let ext = utilities::capitalize_first_ascii(ext);
// Build strict ProgId
let progid = concept::ProgId::new(identifier, &ext, None)?;
// Then build losse ProgId
let losse_progid: lowlevel::LosseProgId = progid.into();
// Return built result
Ok(losse_progid)
}
/// Try converting [Schema] into [Program].
///
/// During this process, some checks will be performed to ensure the validity of the data.
/// For example, the reference to icon, name, or behavior must exist in their respective dictionaries.
/// The identifier must be suit for building ProgId.
pub fn new(schema: Schema) -> Result<Self, ParseProgramError> {
// Extract file name part and directory name part respectively.
let schema_path = Path::new(&schema.path);
let app_path = schema.path.clone();
let app_file_name = Self::extract_file_name(schema_path)?;
let app_file_name = String::from(utilities::osstr_to_str(app_file_name)?);
let app_dir_path = Self::extract_dir_path(schema_path)?;
let app_dir_path = String::from(utilities::osstr_to_str(app_dir_path)?);
// Build app paths key and applications key respectively
let key = concept::FileName::new(&app_file_name)?;
let app_paths_key = lowlevel::AppPathsKey::new(key.clone());
let applications_key = lowlevel::ApplicationsKey::new(key.clone());
// Build string, icon and behavior list,
// and build mapper at the same time.
let (strs, strs_index_map) = Self::flat_hashmap(&schema.strs, |entry| {
let str_res_variant: lowlevel::StrResVariant = entry.as_str().into();
let program_str = ProgramStr {
inner: str_res_variant,
};
Ok(Rc::new(program_str))
})?;
let (icons, icons_index_map) = Self::flat_hashmap(&schema.icons, |entry| {
let icon_res_variant: lowlevel::IconResVariant = entry.as_str().into();
let program_icon = ProgramIcon {
inner: icon_res_variant,
};
Ok(Rc::new(program_icon))
})?;
let (behaviors, behaviors_index_map) = Self::flat_hashmap(&schema.behaviors, |entry| {
// We simply always use "Open" verb.
let cmdline: concept::CmdLine = entry.as_str().parse()?;
let verb = concept::Verb::OPEN.clone();
let shell_verb = lowlevel::ShellVerb::new(verb, cmdline);
let program_behavior = ProgramBehavior { inner: shell_verb };
Ok(Rc::new(program_behavior))
})?;
// Setup default name, icon and behavior
let name = schema
.name
.map(|name| Self::resolve_index(&name, &strs, &strs_index_map))
.transpose()?;
let icon = schema
.icon
.map(|icon| Self::resolve_index(&icon, &icons, &icons_index_map))
.transpose()?;
let behavior = schema
.behavior
.map(|behavior| Self::resolve_index(&behavior, &behaviors, &behaviors_index_map))
.transpose()?;
// We build ProgIdKey and ExtKey list at the same time
let mut ext_keys: Vec<ProgramProgIdExtKey> = Vec::with_capacity(schema.exts.len());
for (key, value) in &schema.exts {
// Build ProgId first.
let progid = Self::build_progid(&schema.identifier, key.as_str())?;
// Then build ProgId key.
let progid_key = lowlevel::ProgIdKey::new(progid);
// Build essential fields for program ProgId key struct.
let name = Self::resolve_index(&value.name, &strs, &strs_index_map)?;
let icon = Self::resolve_index(&value.icon, &icons, &icons_index_map)?;
let behavior = Self::resolve_index(&value.behavior, &behaviors, &behaviors_index_map)?;
// Build Ext.
let ext = concept::Ext::new(key.as_str())?;
let ext_key = lowlevel::ExtKey::new(ext);
// Create program 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);
}
// Everything is okey
Ok(Self {
app_paths_key,
applications_key,
app_path,
app_dir_path,
name,
icon,
behavior,
strs,
icons,
behaviors,
ext_keys,
})
}
}
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_progid_key in &mut self.ext_keys {
let progid_key = &mut program_progid_key.progid_key;
// Create ProgId subkey
debug_println!("Adding ProgId \"{0}\" subkey...", progid_key.inner().to_string());
progid_key.ensure(scope)?;
// Write ProgId values
let name = Some(&program_progid_key.name.inner);
progid_key.set_default(scope, name)?;
progid_key.set_shell_verb(scope, &program_progid_key.behavior.inner)?;
progid_key.set_friendly_type_name(scope, name)?;
progid_key.set_default_icon(scope, Some(&program_progid_key.icon.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> {
todo!()
}
/// Check whether this application has been registered in given view.
///
/// Please note that this is a rough check and do not validate any data.
///
/// The return value only ensures the pre-requirement of `register` and `unregister`.
pub fn is_registered(&self, view: View) -> Result<bool, ProgramError> {
todo!()
}
pub fn link_ext(&self, scope: Scope, index: usize) -> Result<(), ProgramError> {
todo!()
}
pub fn unlink_ext(&self, scope: Scope, index: usize) -> Result<(), ProgramError> {
todo!()
}
pub fn ext_status(&self, view: View, index: usize) -> Result<(), ProgramError> {
todo!()
}
}
// endregion
// region: Program Internals
/// Internal used enum presenting a Program string resource.
#[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: Rc<ProgramStr>,
icon: Rc<ProgramIcon>,
behavior: Rc<ProgramBehavior>,
}
// endregion
pub use schema::{Schema, SchemaError};
pub use program::{Program, ParseProgramError, ProgramError, ProgramSelfExtStatus, ProgramExtStatus};
pub use lowlevel::{Scope, View};
// endregion
+757
View File
@@ -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
+259
View File
@@ -0,0 +1,259 @@
use std::collections::HashMap;
use thiserror::Error as TeError;
use super::{Program, ParseProgramError};
// region: Error Type
/// Error occurs when operating with [Schema].
#[derive(Debug, TeError)]
pub enum SchemaError {
#[error("duplicate key in Schema operation: {0}")]
DuplicateKey(String),
}
// endregion
// region: Schema
/// The sketchpad of complete [Program].
///
/// In suggested usage, we will create a [Schema] first,
/// fill some essential and optional properties,
/// then add file extensions which we need.
/// And finally convert it into immutable [Program] for formal using.
#[derive(Debug)]
pub struct Schema {
identifier: String,
path: String,
clsid: String,
name: Option<String>,
icon: Option<String>,
behavior: Option<String>,
strs: HashMap<String, String>,
icons: HashMap<String, String>,
behaviors: HashMap<String, String>,
exts: HashMap<String, SchemaExt>,
}
impl Schema {
/// Create an empty `Schema` with all fields set to their defaults.
pub fn new() -> Self {
Self {
identifier: String::new(),
path: String::new(),
clsid: String::new(),
name: None,
icon: None,
behavior: None,
strs: HashMap::new(),
icons: HashMap::new(),
behaviors: HashMap::new(),
exts: HashMap::new(),
}
}
/// Try converting [Schema] into [Program].
///
/// This is equivalent to [Program::new].
pub fn into_program(self) -> Result<Program, ParseProgramError> {
Program::new(self)
}
}
impl Schema {
/// Set the identifier of the schema.
///
/// The identifier is used to build ProgId.
/// So it should starts with an ASCII letter and followed by zero or more ASCII letters, digits, underline and hyphen.
/// And it should not be empty.
pub fn set_identifier(&mut self, identifier: &str) -> () {
self.identifier = identifier.to_string();
}
/// Set the absolute path to the executable file.
pub fn set_path(&mut self, exe_path: &str) -> () {
self.path = exe_path.to_string();
}
/// Set the CLSID (Class Identifier) of the application.
///
/// # Todo
///
/// Currently this field is not used by this crate,
/// but you still need to fill it with a legal CLSID for future expanding.
pub fn set_clsid(&mut self, clsid: &str) -> () {
self.clsid = clsid.to_string();
}
/// Set the key referencing a named string resource for the application display name.
///
/// This field is optional and can pass `None` to clear it.
pub fn set_name(&mut self, name: Option<&str>) -> () {
self.name = name.map(|n| n.to_string());
}
/// Set the key referencing a named icon resource for the application default icon.
///
/// This field is optional and can pass `None` to clear it.
pub fn set_icon(&mut self, icon: Option<&str>) -> () {
self.icon = icon.map(|i| i.to_string());
}
/// Set the key referencing a named behavior resource for the application default execution behavior.
///
/// This field is optional and can pass `None` to clear it.
pub fn set_behavior(&mut self, behavior: Option<&str>) -> () {
self.behavior = behavior.map(|b| b.to_string());
}
/// Add a named string resource entry.
///
/// Returns error if the name already exists.
pub fn add_str(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
match self.strs.insert(name.to_string(), value.to_string()) {
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
None => Ok(()),
}
}
/// Add a named icon resource entry.
///
/// Returns error if the name already exists.
pub fn add_icon(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
match self.icons.insert(name.to_string(), value.to_string()) {
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
None => Ok(()),
}
}
/// Add a named behavior (command line) entry.
///
/// Returns error if the name already exists.
pub fn add_behavior(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
match self.behaviors.insert(name.to_string(), value.to_string()) {
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
None => Ok(()),
}
}
/// Add a file extension to the schema.
///
/// The parameter `ext` is the file extension without leading dot `.`.
/// And the parameter `ext_name`, `ext_icon` and `ext_behavior` are the key
/// referencing a named string resource, icon resource and behavior resource respectively.
pub fn add_ext(
&mut self,
ext: &str,
ext_name: &str,
ext_icon: &str,
ext_behavior: &str,
) -> Result<(), SchemaError> {
match self.exts.insert(
ext.to_string(),
SchemaExt::new(ext_name, ext_icon, ext_behavior),
) {
Some(_) => Err(SchemaError::DuplicateKey(ext.to_string())),
None => Ok(()),
}
}
}
impl Schema {
/// Get the identifier of the schema.
pub(super) fn get_identifier(&self) -> &str {
&self.identifier
}
/// Get the absolute path to the executable file.
pub(super) fn get_path(&self) -> &str {
&self.path
}
/// Get the CLSID (Class Identifier) of the application.
pub(super) fn get_clsid(&self) -> &str {
&self.clsid
}
/// Get the key referencing the named string resource for the display name.
/// Return `None` if user doesn't specify this.
pub(super) fn get_name(&self) -> Option<&str> {
self.name.as_ref().map(|v| v.as_str())
}
/// Get the key referencing the named icon resource for the default icon.
/// Return `None` if user doesn't specify this.
pub(super) fn get_icon(&self) -> Option<&str> {
self.icon.as_ref().map(|v| v.as_str())
}
/// Get the key referencing the named behavior resource for the default verb.
/// Return `None` if user doesn't specify this.
pub(super) fn get_behavior(&self) -> Option<&str> {
self.behavior.as_ref().map(|v| v.as_str())
}
/// Get a reference to the string resources map.
pub(super) fn get_strs(&self) -> &HashMap<String, String> {
&self.strs
}
/// Get a reference to the icon resources map.
pub(super) fn get_icons(&self) -> &HashMap<String, String> {
&self.icons
}
/// Get a reference to the behavior resources map.
pub(super) fn get_behaviors(&self) -> &HashMap<String, String> {
&self.behaviors
}
/// Get a reference to the file extensions map.
pub(super) fn get_exts(&self) -> &HashMap<String, SchemaExt> {
&self.exts
}
}
// endregion
// region: Internal Stuff
/// Internal used struct as the Schema file extensions hashmap value type.
#[derive(Debug)]
pub(crate) struct SchemaExt {
name: String,
icon: String,
behavior: String,
}
impl SchemaExt {
/// Create a new `SchemaExt` with the given name, icon key and behavior key.
fn new(name: &str, icon: &str, behavior: &str) -> Self {
Self {
name: name.to_string(),
icon: icon.to_string(),
behavior: behavior.to_string(),
}
}
}
impl SchemaExt {
/// Get the key referencing the named string resource for display name.
pub(super) fn get_name(&self) -> &str {
&self.name
}
/// Get the key referencing the named icon resource for file icon.
pub(super) fn get_icon(&self) -> &str {
&self.icon
}
/// Get the key referencing the named behavior resource.
pub(super) fn get_behavior(&self) -> &str {
&self.behavior
}
}
// endregion
+1 -1
View File
@@ -1,5 +1,5 @@
//! This crate provide utilities fetching and manilupating Windows file association.
//! All code under crate are following Microsoft document: https://learn.microsoft.com/en-us/windows/win32/shell/customizing-file-types-bumper
//! All code under crate are following Microsoft document: <https://learn.microsoft.com/en-us/windows/win32/shell/customizing-file-types-bumper>
#[cfg(not(target_os = "windows"))]
compile_error!("Crate wfassoc is only supported on Windows.");
-501
View File
@@ -1,501 +0,0 @@
//! This crate provide utilities fetching and manilupating Windows file association.
//! All code under crate are following Microsoft document: https://learn.microsoft.com/en-us/windows/win32/shell/customizing-file-types-bumper
#[cfg(not(target_os = "windows"))]
compile_error!("Crate wfassoc is only supported on Windows.");
pub mod extra;
pub mod utilities;
pub mod assoc;
use assoc::{Ext, ProgId};
use indexmap::{IndexMap, IndexSet};
use regex::Regex;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::sync::LazyLock;
use thiserror::Error as TeError;
use winreg::RegKey;
use winreg::enums::{
HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE,
};
// region: Error Types
/// All possible error occurs in this crate.
#[derive(Debug, TeError)]
pub enum Error {
#[error("error occurs when manipulating with Registry: {0}")]
BadRegOper(#[from] std::io::Error),
#[error("{0}")]
CastOsStr(#[from] utilities::CastOsStrError),
#[error("{0}")]
ParseExt(#[from] assoc::ParseExtError),
#[error("no administrative privilege")]
NoPrivilege,
#[error("given identifier \"{0}\" of application is invalid")]
BadIdentifier(String),
#[error("given full path to application is invalid")]
BadFullAppPath,
#[error("manner \"{0}\" is already registered")]
DupManner(String),
#[error("file extension \"{0}\" is already registered")]
DupExt(String),
#[error("the token of manner is invalid")]
InvalidMannerToken,
#[error("the token of file extension is invalid")]
InvalidExtToken,
}
/// The result type used in this crate.
pub type Result<T> = std::result::Result<T, Error>;
// endregion
// region: Types
/// The token for access registered items in Program.
/// This is usually returned when you registering them.
pub type Token = usize;
/// The scope where wfassoc will register and unregister application.
#[derive(Debug, Copy, Clone)]
pub enum Scope {
/// Scope for current user.
User,
/// Scope for all users under this computer.
System,
}
/// The error occurs when cast View into Scope.
#[derive(Debug, TeError)]
#[error("hybrid View can not be cast into Scope")]
pub struct TryFromViewError {}
impl TryFromViewError {
fn new() -> Self {
Self {}
}
}
impl TryFrom<View> for Scope {
type Error = TryFromViewError;
fn try_from(value: View) -> std::result::Result<Self, Self::Error> {
match value {
View::User => Ok(Self::User),
View::System => Ok(Self::System),
View::Hybrid => Err(TryFromViewError::new()),
}
}
}
impl Scope {
/// Check whether we have enough privilege when operating in current scope.
/// If we have, return true, otherwise false.
pub fn has_privilege(&self) -> bool {
// If we operate on System, and we do not has privilege,
// we think we do not have privilege, otherwise,
// there is no privilege required.
!matches!(self, Self::System if !utilities::has_privilege())
}
}
/// The view when wfassoc querying file extension association.
#[derive(Debug, Copy, Clone)]
pub enum View {
/// The view of current user.
User,
/// The view of system.
System,
/// Hybrid view of User and System.
/// It can be seen as that we use System first and then use User to override any existing items.
Hybrid,
}
impl From<Scope> for View {
fn from(value: Scope) -> Self {
match value {
Scope::User => Self::User,
Scope::System => Self::System,
}
}
}
// endregion
// region: Program
/// The struct representing a complete program for registration and unregistration.
#[derive(Debug)]
pub struct Program {
/// The identifier of this program.
identifier: String,
/// The fully qualified path to the application.
full_path: PathBuf,
/// The collection holding all manners of this program.
manners: IndexSet<String>,
/// The collection holding all file extensions supported by this program.
/// The key is file estension and value is its associated manner for opening it.
exts: IndexMap<Ext, Token>,
}
impl Program {
/// Create a new registrar for following operations.
///
/// `identifier` is the unique name of this program.
/// If should only contain digits and alphabet chars,
/// and should not start with any digits.
/// For example, "MyApp" is okey but following names are not okey:
///
/// - `My App`
/// - `3DViewer`
/// - `我的Qt程序` (means "My Qt App" in English)
///
/// More preciously, `identifier` will be used as the vendor part of ProgId.
///
/// `full_path` is the fully qualified path to the application.
pub fn new(identifier: &str, full_path: &str) -> Result<Self> {
// Check identifier
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9]*$").unwrap());
if !RE.is_match(identifier) {
return Err(Error::BadIdentifier(identifier.to_string()));
}
// Everything is okey, build self.
Ok(Self {
identifier: identifier.to_string(),
// The error type of PathBuf FromStr trait is Infallible,
// so it must be okey and we can use unwrap safely.
full_path: full_path.parse().unwrap(),
manners: IndexSet::new(),
exts: IndexMap::new(),
})
}
/// Add manner provided by this program.
pub fn add_manner(&mut self, manner: &str) -> Result<Token> {
// TODO: Use wincmd::CmdArgs instead of String.
// Create manner from string
let manner = manner.to_string();
// Backup a stringfied manner for error output.
let manner_str = manner.to_string();
// Insert manner.
let idx = self.manners.len();
if self.manners.insert(manner) {
Ok(idx)
} else {
Err(Error::DupManner(manner_str))
}
}
/// Get the string display of manner represented by given token
pub fn get_manner_str(&self, token: Token) -> Option<String> {
self.manners.get_index(token).map(|s| s.clone())
}
/// Add file extension supported by this program and its associated manner.
pub fn add_ext(&mut self, ext: &str, token: Token) -> Result<Token> {
// Check manner token
if let None = self.manners.get_index(token) {
return Err(Error::InvalidMannerToken);
}
// Create extension from string
let ext = Ext::new(ext)?;
// Backup a stringfied extension for error output.
let ext_str = ext.to_string();
// Insert file extension
let idx = self.exts.len();
if let None = self.exts.insert(ext, token) {
Ok(idx)
} else {
Err(Error::DupExt(ext_str))
}
}
/// Get the string display of file extension represented by given token
pub fn get_ext_str(&self, token: Token) -> Option<String> {
self.exts.get_index(token).map(|p| p.0.to_string())
}
}
impl Program {
const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths";
const APPLICATIONS: &str = "Software\\Classes\\Applications";
/// Register this application.
pub fn register(&self, scope: Scope) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(Error::NoPrivilege);
}
// Fetch root key.
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
// Fetch file name and start in path.
let file_name = self.extract_file_name()?;
let start_in = self.extract_start_in()?;
// Create App Paths subkey
debug_println!("Adding App Paths subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?;
let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?;
// Write App Paths values
subkey.set_value("", &utilities::path_to_str(&self.full_path)?)?;
subkey.set_value("Path", &utilities::osstr_to_str(&start_in)?)?;
// Create Applications subkey
debug_println!("Adding Applications subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?;
// Write Applications values
if !self.exts.is_empty() {
let (supported_types, _) =
subkey.create_subkey_with_flags("SupportedTypes", KEY_WRITE)?;
for ext in self.exts.keys() {
supported_types.set_value(ext.to_string(), &"")?;
}
}
// Create ProgId subkeys
debug_println!("Adding ProgId subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
for (ext, manner_token) in self.exts.iter() {
let manner = self.manners.get_index(*manner_token).ok_or(Error::InvalidMannerToken)?;
let prog_id = self.build_prog_id(ext);
debug_println!("Adding ProgId \"{0}\" subkey...", prog_id.to_string());
let (subkey, _) = subkey_parent.create_subkey_with_flags(prog_id.to_string(), KEY_READ)?;
let (subkey_verb, _) = subkey.create_subkey_with_flags("open", KEY_READ)?;
let (subkey_command, _) = subkey_verb.create_subkey_with_flags("command", KEY_WRITE)?;
subkey_command.set_value("", manner)?;
}
// Okey
utilities::notify_assoc_changed();
Ok(())
}
/// Unregister this application.
pub fn unregister(&self, scope: Scope) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(Error::NoPrivilege);
}
// Fetch root key and file name.
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let file_name = self.extract_file_name()?;
// Remove App Paths subkey
debug_println!("Removing App Paths subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_WRITE)?;
subkey_parent.delete_subkey_all(file_name)?;
// Remove Applications subkey
debug_println!("Removing Applications subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
subkey_parent.delete_subkey_all(file_name)?;
// Remove ProgId subkeys
debug_println!("Removing ProgId subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
for ext in self.exts.keys() {
let prog_id = self.build_prog_id(ext);
debug_println!("Removing ProgId \"{0}\" subkey...", prog_id.to_string());
subkey_parent.delete_subkey_all(prog_id.to_string())?;
}
// Okey
utilities::notify_assoc_changed();
Ok(())
}
/// Check whether this application has been registered.
///
/// Please note that this is a rough check and do not validate any data.
pub fn is_registered(&self, scope: Scope) -> Result<bool> {
// Fetch root key and file name.
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let file_name = self.extract_file_name()?;
// Check App Paths subkey.
debug_println!("Checking App Paths subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?;
if let Err(_) = subkey_parent.open_subkey_with_flags(file_name, KEY_READ) {
return Ok(false);
}
// Check Application subkey.
debug_println!("Checking Applications subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
if let Err(_) = subkey_parent.open_subkey_with_flags(file_name, KEY_READ) {
return Ok(false);
}
// Both subkeys are roughly existing.
Ok(true)
}
}
impl Program {
const CLASSES: &str = "Software\\Classes";
/// Set the default "open with" of given token associated extension to this program.
pub fn link_ext(&self, ext: Token, scope: Scope) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(Error::NoPrivilege);
}
// Fetch file extension and build ProgId from it
let (ext, _) = match self.exts.get_index(ext) {
Some(v) => v,
None => return Err(Error::InvalidExtToken),
};
let prog_id = self.build_prog_id(ext);
// Fetch root key and navigate to Classes
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let classes = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
// Open or create this extension key
let (subkey, _) = classes.create_subkey_with_flags(ext.to_string(), KEY_WRITE)?;
// Set the default way to open this file extension
subkey.set_value("", &prog_id.to_string())?;
// Okey
Ok(())
}
/// Remove this program from the default "open with" of given token associated extension.
///
/// If the default "open with" of given extension is not our program,
/// or there is no such file extension, this function do nothing.
pub fn unlink_ext(&self, ext: Token, scope: Scope) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(Error::NoPrivilege);
}
// Fetch file extension and build ProgId from it
let (ext, _) = match self.exts.get_index(ext) {
Some(v) => v,
None => return Err(Error::InvalidExtToken),
};
let prog_id = self.build_prog_id(ext);
// Fetch root key and navigate to Classes
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let classes = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
// Open key for this extension.
// If there is no such key, return directly.
if let Some(subkey) =
extra::winreg::try_open_subkey_with_flags(&classes, ext.to_string(), KEY_WRITE)?
{
// Only delete the default key if it is equal to our ProgId
if let Some(value) = extra::winreg::try_get_value::<String, _>(&subkey, "")? {
if value == prog_id.to_string() {
// Delete the default key.
subkey.delete_value("")?;
}
}
}
// Okey
Ok(())
}
/// Query the default "open with" of given token associated extension.
///
/// This function will return its associated ProgId if it is existing.
pub fn query_ext(&self, ext: Token, view: View) -> Result<Option<ProgId>> {
// Fetch file extension
let (ext, _) = match self.exts.get_index(ext) {
Some(v) => v,
None => return Err(Error::InvalidExtToken),
};
// Fetch root key and navigate to Classes
let hk = match view {
View::User => RegKey::predef(HKEY_CURRENT_USER),
View::System => RegKey::predef(HKEY_LOCAL_MACHINE),
View::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
};
let classes = match view {
View::User | View::System => hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?,
View::Hybrid => hk.open_subkey_with_flags("", KEY_READ)?,
};
// Open key for this extension if possible
let rv =
match extra::winreg::try_open_subkey_with_flags(&classes, ext.to_string(), KEY_READ)? {
Some(subkey) => {
// Try get associated ProgId if possible
match extra::winreg::try_get_value::<String, _>(&subkey, "")? {
Some(value) => Some(ProgId::from(value.as_str())),
None => None,
}
}
None => None,
};
// Okey
Ok(rv)
}
}
impl Program {
/// Extract the file name part from full path to application,
/// which was used in Registry path component.
fn extract_file_name(&self) -> Result<&OsStr> {
// Get the file name part and make sure it is not empty.
// Empty checker is CRUCIAL!
self.full_path
.file_name()
.and_then(|p| if p.is_empty() { None } else { Some(p) })
.ok_or(Error::BadFullAppPath)
}
/// Extract the start in path from full path to application,
/// which basically is the stem of full path.
fn extract_start_in(&self) -> Result<&OsStr> {
// Get parent part and make sure it is not empty
// Empty checker is CRUCIAL!
self.full_path
.parent()
.map(|p| p.as_os_str())
.and_then(|p| if p.is_empty() { None } else { Some(p) })
.ok_or(Error::BadFullAppPath)
}
/// Build ProgId from identifier and given file extension.
fn build_prog_id(&self, ext: &Ext) -> ProgId {
ProgId::Std(assoc::StdProgId::new(
&self.identifier,
&utilities::capitalize_first_ascii(ext.inner()),
None,
))
}
}
// endregion
-582
View File
@@ -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
+199 -590
View File
@@ -1,34 +1,49 @@
//! The module containing lowlevel operation involving file association.
use crate::win32::{concept, regext, utilities};
use std::fmt::Display;
use std::str::FromStr;
use thiserror::Error as TeError;
use winreg::RegKey;
use winreg::enums::{
HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE,
};
use winreg::enums::{KEY_READ, KEY_WRITE};
// region: Error Type
// region: Error and Result Types
/// Error occurs in this module.
#[derive(Debug, TeError)]
pub enum Error {
#[error("can not perform this operation because lack essential privilege")]
#[error("some lowlevel operations require escalating privilege")]
NoPrivilege,
#[error("given registry key is inexistant")]
#[error("given lowlevel registry key is not presented")]
InexistantKey,
#[error("registry operation error: {0}")]
#[error("lowlevel registry operation error: {0}")]
BadRegOp(#[from] std::io::Error),
#[error("{0}")]
#[error("unexpected blank registry key in lowlevel registry operation")]
UnexpectedBlankKey(#[from] regext::BlankPathError),
#[error("{0}")]
#[error("lowlevel operation error: {0}")]
ExpandEnvVar(#[from] concept::ExpandEnvVarError),
#[error("lowlevel operation error: {0}")]
LoadIconRc(#[from] concept::LoadIconRcError),
#[error("{0}")]
#[error("lowlevel operation error: {0}")]
LoadStrRc(#[from] concept::LoadStrRcError),
#[error("lowlevel operation error: {0}")]
ParseVerb(#[from] concept::ParseVerbError),
#[error("lowlevel operation error: {0}")]
ParseCmdLine(#[from] concept::ParseCmdLineError),
#[error("lowlevel operation error: {0}")]
ParseExt(#[from] concept::ParseExtError),
}
/// The result type used in this module and all sub-modules.
type Result<T> = std::result::Result<T, Error>;
// endregion
// region: Scope and View
// region: Exposed Stuff
// region: Scope
/// The scope where wfassoc will register and unregister.
#[derive(Debug, Copy, Clone)]
@@ -39,6 +54,34 @@ pub enum Scope {
System,
}
/// The error occurs when cast View into Scope.
#[derive(Debug, TeError)]
#[error("hybrid view can not be cast into any scope")]
pub struct TryFromViewError {}
impl TryFromViewError {
/// Creates a new `TryFromViewError`.
fn new() -> Self {
Self {}
}
}
impl TryFrom<View> for Scope {
type Error = TryFromViewError;
fn try_from(value: View) -> std::result::Result<Self, Self::Error> {
match value {
View::User => Ok(Self::User),
View::System => Ok(Self::System),
View::Hybrid => Err(TryFromViewError::new()),
}
}
}
// endregion
// region: View
/// The view when wfassoc querying infomations.
#[derive(Debug, Copy, Clone)]
pub enum View {
@@ -51,17 +94,6 @@ pub enum View {
Hybrid,
}
/// The error occurs when cast View into Scope.
#[derive(Debug, TeError)]
#[error("hybrid view can not be cast into any scope")]
pub struct TryFromViewError {}
impl TryFromViewError {
fn new() -> Self {
Self {}
}
}
impl From<Scope> for View {
fn from(value: Scope) -> Self {
match value {
@@ -71,29 +103,19 @@ impl From<Scope> for View {
}
}
impl TryFrom<View> for Scope {
type Error = TryFromViewError;
fn try_from(value: View) -> Result<Self, Self::Error> {
match value {
View::User => Ok(Self::User),
View::System => Ok(Self::System),
View::Hybrid => Err(TryFromViewError::new()),
}
}
}
// endregion
// region: Exposed Structs (Losse, Variant and others)
// region: Losse ProgId
/// The enum representing a losse Programmatic Identifiers (ProgId).
///
/// In real world, not all software developers are willing to following Microsoft suggestions to use ProgId.
/// They use string which do not have any regulation as ProgId.
/// In real world, not all software developers are willing
/// to following Microsoft suggestions to use ProgId.
/// They frequently use string which do not have any regulation as ProgId.
/// This enum is designed for handling this scenario.
///
/// You can utilize [FromStr] trait to build this struct.
/// Standard ProgId and any casual string are both acceptable.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LosseProgId {
Plain(String),
@@ -136,6 +158,11 @@ impl From<concept::ProgId> for LosseProgId {
/// 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),
@@ -143,10 +170,26 @@ pub enum IconResVariant {
}
impl IconResVariant {
pub fn extract(&self, kind: concept::IconSizeKind) -> Result<concept::IconRc, Error> {
/// 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) => concept::IconRc::new(v.get_path(), v.get_index(), 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)
}
@@ -188,6 +231,9 @@ impl From<concept::IconRefStr> for IconResVariant {
/// 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),
@@ -195,13 +241,27 @@ pub enum StrResVariant {
}
impl StrResVariant {
pub fn extract(&self) -> Result<String, Error> {
/// 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) => {
let rc = concept::StrRc::new(v.get_path(), v.get_index())?;
// 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()
}
};
@@ -241,6 +301,12 @@ impl From<concept::StrRefStr> for StrResVariant {
// 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,
@@ -248,14 +314,17 @@ pub struct ShellVerb {
}
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
}
@@ -265,12 +334,37 @@ impl ShellVerb {
// endregion
// region: Utilities
// region: Internal Stuff
/// The territory of opening key.
// region: Opened Key
/// Internal used struct representing the result about opening registry key in given scope or view.
#[derive(Debug)]
struct OpenedKey {
/// The parent key of opened key which must be presented.
parent_key: RegKey,
/// The opened key which may not be presented in registry.
this_key: Option<RegKey>,
}
impl OpenedKey {
/// Creates a new `OpenedKey` with the given parent key and optional opened key.
fn new(parent_key: RegKey, this_key: Option<RegKey>) -> Self {
Self {
parent_key,
this_key,
}
}
}
// endregion
// region: Open Key Territory
/// Internal used enum representing the territory of opening registry key.
///
/// Scope and View will finally be converted into this type,
/// and delivered to an uniform function to open key.
/// [Scope] and [View] will finally be converted into this type,
/// and delivered to an uniform function to open registry key.
#[derive(Debug, Copy, Clone)]
enum OpenKeyTerritory {
System,
@@ -297,7 +391,11 @@ impl From<View> for OpenKeyTerritory {
}
}
/// The purpose of opening this key.
// endregion
// region: Open Key Purpose
/// The purpose of opening given registry key.
#[derive(Debug, Copy, Clone)]
enum OpenKeyPurpose {
/// Only read something.
@@ -306,9 +404,30 @@ enum OpenKeyPurpose {
ReadWrite,
}
impl OpenKeyPurpose {
/// Converts this purpose to the corresponding registry permission flags.
fn to_permission(&self) -> u32 {
match self {
OpenKeyPurpose::Read => PERM_R,
OpenKeyPurpose::ReadWrite => PERM_RW,
}
}
}
// endregion
// region: Permission Constants
/// The read-only permission when opening Windows Registry.
const PERM_R: u32 = KEY_READ;
/// The read-write permission when opening Windows Registry.
const PERM_RW: u32 = KEY_READ | KEY_WRITE;
// endregion
/// Check whether we have enough privilege when operating in given territory and purpose.
/// If we have, simply return, otherwise return error.
fn check_privilege(territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<(), Error> {
fn check_privilege(territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<()> {
match purpose {
// Read is okey for every territory.
OpenKeyPurpose::Read => Ok(()),
@@ -328,556 +447,46 @@ fn check_privilege(territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Resu
}
}
/// Internal used struct representing the result of opening scope or view.
#[derive(Debug)]
struct OpenedKey {
/// The parent key of opened key which must be presented.
parent_key: RegKey,
/// The opened key which may not be presented in registry.
this_key: Option<RegKey>,
}
// TODO:
// This function should be eliminated or moved to another place,
// once we finish the quote mechanism in StrRefStr and IconRefStr.
impl OpenedKey {
fn new(parent_key: RegKey, this_key: Option<RegKey>) -> Self {
Self {
parent_key,
this_key,
}
/// Internal function removing quote pair if possible.
///
/// # Todo
///
/// This function actually is the compromise about the current design of this crate.
/// This function will be optimized in future.
///
/// In some cases, the path part of [concept::IconRefStr] or [concept::StrRefStr] is quoted by quote.
/// This can no be recognized by Win32 functions.
/// So in this case, we should remove this quote pair.
fn strip_quote<'a>(s: &'a str) -> &'a str {
let bytes = s.as_bytes();
if bytes.len() >= 2 && bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"' {
// Because quote is 1 byte in UTF8, we can safely slice it,
// without worry about panic.
&s[1..s.len() - 1]
} else {
s
}
}
// endregion
// region: App Paths Key
// region: Registry Keys
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AppPathsKey {
key_name: concept::FileName,
}
// YYC MARK:
// Load submodules and re-export them.
impl AppPathsKey {
pub fn new(inner: concept::FileName) -> Self {
Self { key_name: inner }
}
mod app_path_key;
mod applications_key;
mod ext_key;
mod progid_key;
pub fn inner(&self) -> &concept::FileName {
&self.key_name
}
}
impl AppPathsKey {
const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths";
fn open_key(
&self,
territory: OpenKeyTerritory,
purpose: OpenKeyPurpose,
) -> Result<OpenedKey, Error> {
// check privilege
check_privilege(territory, purpose)?;
// Fetch the permission
let perms = match purpose {
OpenKeyPurpose::Read => KEY_READ,
OpenKeyPurpose::ReadWrite => KEY_READ | KEY_WRITE,
};
// get the root key
let hk = match territory {
OpenKeyTerritory::User => RegKey::predef(HKEY_CURRENT_USER),
OpenKeyTerritory::System => RegKey::predef(HKEY_LOCAL_MACHINE),
// There is no way that territory is hybrid.
OpenKeyTerritory::Hybrid => panic!("unexpected hybrid key territory"),
};
// navigate to App Paths
let app_paths = hk.open_subkey_with_flags(Self::APP_PATHS, perms)?;
// open file name key if possible
let this_app = regext::try_open_subkey_with_flags(
&app_paths,
regext::blank_path_guard(self.key_name.inner())?,
perms,
)?;
// okey
Ok(OpenedKey::new(app_paths, this_app))
}
fn open_scope_for_read(&self, scope: Scope) -> Result<OpenedKey, Error> {
self.open_key(scope.into(), OpenKeyPurpose::Read)
}
fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey, Error> {
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite)
}
pub fn is_exist(&self, scope: Scope) -> Result<bool, Error> {
let key = self.open_scope_for_read(scope)?.this_key;
Ok(key.is_some())
}
/// Ensure this application key is presented in App Paths.
///
/// Return true if we newly create this key,
/// otherwise false indicating there already is an existing key.
pub fn ensure(&mut self, scope: Scope) -> Result<bool, Error> {
let key = self.open_scope_for_write(scope)?;
if let None = key.this_key {
let _ = key.parent_key.create_subkey_with_flags(
regext::blank_path_guard(self.key_name.inner())?,
KEY_READ | KEY_WRITE,
)?;
Ok(true)
} else {
Ok(false)
}
}
/// Delete this application key from App Paths.
///
/// If there is no such key in App Paths,
/// this function does nothing.
pub fn delete(&mut self, scope: Scope) -> Result<(), Error> {
let key = self.open_scope_for_write(scope)?;
key.parent_key
.delete_subkey_all(regext::blank_path_guard(self.key_name.inner())?)?;
Ok(())
}
fn open_scope_for_getter(&self, scope: Scope) -> Result<RegKey, Error> {
self.open_scope_for_read(scope)?
.this_key
.ok_or(Error::InexistantKey)
}
fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey, Error> {
self.open_scope_for_write(scope)?
.this_key
.ok_or(Error::InexistantKey)
}
const NAMEOF_DEFAULT: &str = "";
///
///
/// This field point to the fully qualified path to the application.
pub fn get_default(&self, scope: Scope) -> Result<String, Error> {
let key = self.open_scope_for_getter(scope)?;
Ok(key.get_value(Self::NAMEOF_DEFAULT)?)
}
///
///
/// This field should be filled with fully qualified path to the application.
pub fn set_default(&mut self, scope: Scope, value: &str) -> Result<(), Error> {
let key = self.open_scope_for_setter(scope)?;
key.set_value(Self::NAMEOF_DEFAULT, &value)?;
Ok(())
}
const NAMEOF_PATH: &str = "Path";
///
///
/// This field point to the added path for PATH environment variable.
/// Usually it is the path to application directory.
pub fn get_path(&self, scope: Scope) -> Result<String, Error> {
let key = self.open_scope_for_getter(scope)?;
Ok(key.get_value(Self::NAMEOF_PATH)?)
}
///
///
/// This field should be the added path for PATH environment variable.
/// Usually it is the path to application directory.
pub fn set_path(&mut self, scope: Scope, value: &str) -> Result<(), Error> {
let key = self.open_scope_for_setter(scope)?;
key.set_value(Self::NAMEOF_PATH, &value)?;
Ok(())
}
}
// endregion
// region: Applications Key
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ApplicationsKey {
key_name: concept::FileName,
}
impl ApplicationsKey {
pub fn new(inner: concept::FileName) -> Self {
Self { key_name: inner }
}
pub fn inner(&self) -> &concept::FileName {
&self.key_name
}
}
impl ApplicationsKey {
const FULL_APPLICATIONS: &str = "Software\\Classes\\Applications";
const PARTIAL_APPLICATIONS: &str = "Applications";
fn open_key(
&self,
territory: OpenKeyTerritory,
purpose: OpenKeyPurpose,
) -> Result<OpenedKey, Error> {
// check privilege
check_privilege(territory, purpose)?;
// Fetch the permission
let perms = match purpose {
OpenKeyPurpose::Read => KEY_READ,
OpenKeyPurpose::ReadWrite => KEY_READ | KEY_WRITE,
};
// get the root key
let hk = match territory {
OpenKeyTerritory::User => RegKey::predef(HKEY_CURRENT_USER),
OpenKeyTerritory::System => RegKey::predef(HKEY_LOCAL_MACHINE),
OpenKeyTerritory::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
};
let applications = match territory {
OpenKeyTerritory::User | OpenKeyTerritory::System => {
hk.open_subkey_with_flags(Self::FULL_APPLICATIONS, perms)?
}
OpenKeyTerritory::Hybrid => {
hk.open_subkey_with_flags(Self::PARTIAL_APPLICATIONS, perms)?
}
};
// open app key if possible
let this_app = regext::try_open_subkey_with_flags(
&applications,
regext::blank_path_guard(self.key_name.inner())?,
perms,
)?;
// okey
Ok(OpenedKey::new(applications, this_app))
}
fn open_view_for_read(&self, view: View) -> Result<OpenedKey, Error> {
self.open_key(view.into(), OpenKeyPurpose::Read)
}
fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey, Error> {
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite)
}
pub fn is_exist(&self, view: View) -> Result<bool, Error> {
let key = self.open_view_for_read(view)?.this_key;
Ok(key.is_some())
}
pub fn ensure(&mut self, scope: Scope) -> Result<bool, Error> {
let key = self.open_scope_for_write(scope)?;
if let None = key.this_key {
let _ = key.parent_key.create_subkey_with_flags(
regext::blank_path_guard(self.key_name.inner())?,
KEY_READ | KEY_WRITE,
)?;
Ok(true)
} else {
Ok(false)
}
}
pub fn delete(&mut self, scope: Scope) -> Result<(), Error> {
let key = self.open_scope_for_write(scope)?;
key.parent_key
.delete_subkey_all(regext::blank_path_guard(self.key_name.inner())?)?;
Ok(())
}
fn open_view_for_getter(&self, view: View) -> Result<RegKey, Error> {
self.open_view_for_read(view)?
.this_key
.ok_or(Error::InexistantKey)
}
fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey, Error> {
self.open_scope_for_write(scope)?
.this_key
.ok_or(Error::InexistantKey)
}
const NAMEOF_SHELL_VERB_PART1: &str = "shell";
const NAMEOF_SHELL_VERB_PART3: &str = "command";
pub fn get_shell_verb(&self, view: View) -> Result<Option<ShellVerb>, Error> {
todo!()
}
pub fn set_shell_verb(&mut self, scope: Scope, sv: Option<&ShellVerb>) -> Result<(), Error> {
todo!()
}
const NAMEOF_DEFAULT_ICON: &str = "DefaultIcon";
pub fn get_default_icon(&self, view: View) -> Result<Option<IconResVariant>, Error> {
todo!()
}
pub fn set_default_icon(
&mut self,
scope: Scope,
icon: Option<&IconResVariant>,
) -> Result<(), Error> {
todo!()
}
const NAMEOF_FRIENDLY_APP_NAME: &str = "FriendlyAppName";
pub fn get_friendly_app_name(&self, view: View) -> Result<Option<StrResVariant>, Error> {
todo!()
}
pub fn set_friendly_app_name(
&mut self,
scope: Scope,
name: Option<&StrResVariant>,
) -> Result<(), Error> {
todo!()
}
const NAMEOF_SUPPORTED_TYPES: &str = "SupportedTypes";
pub fn get_supported_types(&self, view: View) -> Result<Option<Vec<concept::Ext>>, Error> {
todo!()
}
pub fn set_supported_types(
&mut self,
scope: Scope,
tys: Option<&[&concept::Ext]>,
) -> Result<(), Error> {
// TODO: If given slice is empty, do not create this key.
todo!()
}
const NAMEOF_NO_OPEN_WITH: &str = "NoOpenWith";
pub fn get_no_open_with(&self, view: View) -> Result<bool, Error> {
todo!()
}
pub fn set_no_open_with(&mut self, scope: Scope, flag: bool) -> Result<(), Error> {
todo!()
}
}
// endregion
// region: File Extension Key
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ExtKey {
ext: concept::Ext,
}
impl ExtKey {
pub fn new(inner: concept::Ext) -> Self {
Self { ext: inner }
}
pub fn inner(&self) -> &concept::Ext {
&self.ext
}
}
impl ExtKey {
const FULL_CLASSES: &str = "Software\\Classes";
const PARTIAL_CLASSES: &str = "";
fn open_key(
&self,
territory: OpenKeyTerritory,
purpose: OpenKeyPurpose,
) -> Result<OpenedKey, Error> {
// check privilege
check_privilege(territory, purpose)?;
// Fetch the permission
let perms = match purpose {
OpenKeyPurpose::Read => KEY_READ,
OpenKeyPurpose::ReadWrite => KEY_READ | KEY_WRITE,
};
// navigate to extension container
let hk = match territory {
OpenKeyTerritory::User => RegKey::predef(HKEY_CURRENT_USER),
OpenKeyTerritory::System => RegKey::predef(HKEY_LOCAL_MACHINE),
OpenKeyTerritory::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
};
let classes = match territory {
OpenKeyTerritory::User | OpenKeyTerritory::System => {
hk.open_subkey_with_flags(Self::FULL_CLASSES, perms)?
}
OpenKeyTerritory::Hybrid => hk.open_subkey_with_flags(Self::PARTIAL_CLASSES, perms)?,
};
// open extension key if possible
let this_ext = regext::try_open_subkey_with_flags(
&classes,
regext::blank_path_guard(self.ext.dotted_inner())?,
perms,
)?;
// okey
Ok(OpenedKey::new(classes, this_ext))
}
pub fn is_exist(&self, view: View) -> Result<bool, Error> {
todo!()
}
pub fn ensure(&mut self, scope: Scope) -> Result<bool, Error> {
todo!()
}
pub fn delete(&mut self, scope: Scope) -> Result<(), Error> {
todo!()
}
// YYC MARK:
// We do not support "Content Type" and "PerceivedType"
// because current interface are enough to use,
// and these types has not been made as concept struct in Rust.
const NAMEOF_DEFAULT: &str = "";
pub fn get_default(&self, view: View) -> Result<Option<LosseProgId>, Error> {
todo!()
}
pub fn set_default(&mut self, scope: Scope, pid: Option<&LosseProgId>) -> Result<(), Error> {
todo!()
}
const NAMEOF_OPEN_WITH_PROGIDS: &str = "OpenWithProgIds";
pub fn get_open_with_progids(&self, view: View) -> Result<Vec<LosseProgId>, Error> {
todo!()
}
pub fn is_in_open_with_progids(&self, view: View, pid: &LosseProgId) -> Result<bool, Error> {
todo!()
}
pub fn add_into_open_with_progids(
&mut self,
scope: Scope,
pid: &LosseProgId,
) -> Result<(), Error> {
todo!()
}
pub fn remove_from_open_with_progids(
&mut self,
scope: Scope,
pid: &LosseProgId,
) -> Result<(), Error> {
todo!()
}
}
// endregion
// region: ProgId Key
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ProgIdKey {
progid: LosseProgId,
}
impl ProgIdKey {
pub fn new(inner: LosseProgId) -> Self {
Self { progid: inner }
}
pub fn inner(&self) -> &LosseProgId {
&self.progid
}
}
impl ProgIdKey {
const FULL_CLASSES: &str = "Software\\Classes";
const PARTIAL_CLASSES: &str = "";
fn open_key(
&self,
territory: OpenKeyTerritory,
purpose: OpenKeyPurpose,
) -> Result<OpenedKey, Error> {
todo!()
}
pub fn is_exist(&self, view: View) -> Result<bool, Error> {
todo!()
}
pub fn ensure(&mut self, scope: Scope) -> Result<bool, Error> {
todo!()
}
pub fn delete(&mut self, scope: Scope) -> Result<(), Error> {
todo!()
}
// YYC MARK:
// Currently we only support (Default), FriendlyTypeName and DefaultIcon
// to just cover the basic usage.
const NAMEOF_DEFAULT: &str = "";
pub fn get_default(&self, view: View) -> Result<Option<StrResVariant>, Error> {
todo!()
}
///
///
/// The legacy way to set friendly name for this ProgId.
pub fn set_default(&mut self, scope: Scope, name: Option<&StrResVariant>) -> Result<(), Error> {
todo!()
}
const NAMEOF_SHELL_VERB_PART1: &str = "shell";
const NAMEOF_SHELL_VERB_PART3: &str = "command";
pub fn get_shell_verb(&self, view: View) -> Result<ShellVerb, Error> {
todo!()
}
pub fn set_shell_verb(&mut self, scope: Scope, sv: &ShellVerb) -> Result<(), Error> {
todo!()
}
const NAMEOF_FRIENDLY_TYPE_NAME: &str = "FriendlyTypeName";
pub fn get_friendly_type_name(&self, view: View) -> Result<Option<StrResVariant>, Error> {
todo!()
}
///
///
/// Set this entry to a friendly name for the ProgID.
pub fn set_friendly_type_name(
&mut self,
scope: Scope,
name: Option<&StrResVariant>,
) -> Result<(), Error> {
todo!()
}
const NAMEOF_DEFAULT_ICON: &str = "DefaultIcon";
pub fn get_default_icon(&self, view: View) -> Result<Option<IconResVariant>, Error> {
todo!()
}
pub fn set_default_icon(
&mut self,
scope: Scope,
icon: Option<&IconResVariant>,
) -> Result<(), Error> {
todo!()
}
}
pub use app_path_key::AppPathsKey;
pub use applications_key::ApplicationsKey;
pub use ext_key::ExtKey;
pub use progid_key::ProgIdKey;
// endregion
+175
View File
@@ -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(())
}
}
+402
View File
@@ -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(())
}
}
+251
View File
@@ -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(())
}
}
+364
View File
@@ -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(())
}
}
+6 -3
View File
@@ -5,11 +5,11 @@ use std::iter::FusedIterator;
use std::path::Path;
use thiserror::Error as TeError;
// region: OS String Related
// region: OS String Casting
/// The error occurs when casting `OsStr` into `str`.
/// The error occurs when casting [OsStr] into [str].
#[derive(Debug, TeError)]
#[error("fail to cast OS string into string")]
#[error("fail to cast OS string into Rust string")]
pub struct CastOsStrError {}
impl CastOsStrError {
@@ -32,6 +32,8 @@ pub fn osstr_to_str(osstr: &OsStr) -> Result<&str, CastOsStrError> {
// region: Capitalize First ASCII Letter
/// The struct transforming accept an iterator and perform as an iterator
/// whose transform the first character of accepted iterator into capital form (ASCII char only).
struct CapitalizeFirstAscii<T>
where
T: Iterator<Item = char>,
@@ -72,6 +74,7 @@ where
impl<T> FusedIterator for CapitalizeFirstAscii<T> where T: Iterator<Item = char> + FusedIterator {}
/// Accept an string slice, transform its first char (ASCII only) as capital form and output.
pub fn capitalize_first_ascii(s: &str) -> String {
CapitalizeFirstAscii::new(s.chars()).collect()
}
+3
View File
@@ -1,3 +1,6 @@
//! The module containing Win32 stuff related to file association operation,
//! including Win32 concept, Registry operation helper and etc.
pub mod concept;
pub mod utilities;
pub mod regext;
+257 -47
View File
@@ -15,7 +15,7 @@ use windows_sys::Win32::UI::WindowsAndMessaging::HICON;
/// The error occurs when constructing Ext with bad body.
#[derive(Debug, TeError)]
#[error("given file extension body \"{inner}\" is invalid")]
#[error(r#"given file extension body "{inner}" is invalid"#)]
pub struct BadExtBodyError {
/// The clone of string which is not a valid file extension body.
inner: String,
@@ -32,6 +32,12 @@ impl BadExtBodyError {
/// The struct representing an file extension which must start with dot (`.`)
/// and followed by at least one arbitrary characters.
///
/// If you having a file extension expressed in string form with leading dot,
/// please utilize [FromStr] trait to build this struct.
/// Otherwise, use [Ext::new] with file extension without leading dot.
///
/// The [Display] trait this struct implemented always outputs file extension with leading dot.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Ext {
/// The body of file extension (excluding dot).
@@ -42,6 +48,8 @@ impl Ext {
/// Create an new file extension.
///
/// `body` is the body of file extension (excluding dot, such as `jpg`).
/// Empty body is not allowed.
///
/// If you want to create this struct with ordinary extension string like `.jpg`,
/// please use `Ext::from_str()` or `parse::<Ext>()` instead.
pub fn new(body: &str) -> Result<Self, BadExtBodyError> {
@@ -56,11 +64,17 @@ impl Ext {
}
/// Get the body part of file extension (excluding dot)
///
/// This struct promise that there is no possibility that return value is blank
/// (i.e. the body of file extension is empty).
pub fn inner(&self) -> &str {
&self.body
}
/// Get the body part of file extension (with leading dot)
///
/// This struct promise that there is no possibility that return value is dot only
/// (i.e. the body of file extension is empty).
pub fn dotted_inner(&self) -> String {
// Reuse Display trait result
self.to_string()
@@ -69,7 +83,7 @@ impl Ext {
/// The error occurs when try parsing string into FileExt.
#[derive(Debug, TeError)]
#[error("given file extension name \"{inner}\" is invalid")]
#[error(r#"given file extension name "{inner}" is invalid"#)]
pub struct ParseExtError {
/// The clone of string which is not a valid file extension.
inner: String,
@@ -110,7 +124,7 @@ impl FromStr for Ext {
/// The error occurs when constructing ProgId.
#[derive(Debug, TeError)]
#[error("given ProgId part \"{inner}\" is invalid")]
#[error(r#"given ProgId part "{inner}" is invalid"#)]
pub struct BadProgIdPartError {
/// The clone of string which is not a valid ProgId part.
inner: String,
@@ -127,16 +141,21 @@ impl BadProgIdPartError {
/// The ProgId exactly follows Microsoft suggested
/// `[Vendor or Application].[Component].[Version]` format.
/// And additionally, `[Version]` part is optional.
///
/// Additionally, `[Version]` part is optional.
///
/// However, most of applications do no follow this standard,
/// this scenario is not convered by this struct in there.
/// In reality world, most of applications do no follow this standard.
/// However, this scenario is not convered by this struct in there.
/// It should be done by other structs in other places.
///
/// If you have a ProgId expressed in string form, please utilize [FromStr] trait to parse it.
/// Or use [ProgId::new] with each parts to construct this struct.
///
/// The [Display] trait this struct implemented always
/// outputs legal ProgId in string form.
///
/// Reference:
/// - https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids
/// - https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
/// - <https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids>
/// - <https://learn.microsoft.com/en-us/windows/win32/com/-progid--key>
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ProgId {
/// The vendor part of ProgId.
@@ -149,6 +168,9 @@ pub struct ProgId {
impl ProgId {
/// Create a new ProgId with given parts.
///
/// Empty string or string with embedded dot is not allowed
/// for vendor and component parts.
pub fn new(
vendor: &str,
component: &str,
@@ -186,7 +208,7 @@ impl ProgId {
/// The error occurs when parsing ProgId.
#[derive(Debug, TeError)]
#[error("given ProgId \"{inner}\" is invalid")]
#[error(r#"given ProgId "{inner}" is invalid"#)]
pub struct ParseProgIdError {
/// The clone of string which is not a valid ProgId.
inner: String,
@@ -245,6 +267,14 @@ impl FromStr for ProgId {
/// `{26EE0668-A00A-44D7-9371-BEB064C98683}` (case insensitive).
///
/// Please note that the curly brace is the essential part of CLSID.
///
/// If you have a CLSID expressed in string form,
/// please utilize [FromStr] trait to parse it.
/// You also can utilize [Clsid::new] with [uuid] crate to create a custom CLSID,
/// or directly use [Clsid::with_random] to create a random new one.
///
/// The [Display] trait this struct implemented always
/// output CLSID in lower-case with curly brace.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Clsid {
inner: Uuid,
@@ -252,7 +282,7 @@ pub struct Clsid {
impl Clsid {
/// Create new CLSID from underlying UUID.
fn new(uuid: &Uuid) -> Self {
pub fn new(uuid: &Uuid) -> Self {
Self { inner: *uuid }
}
@@ -264,7 +294,7 @@ impl Clsid {
/// The error occurs when parsing CLSID.
#[derive(Debug, TeError)]
#[error("given string \"{inner}\" is invalid for uuid")]
#[error(r#"given string "{inner}" is invalid for CLSID"#)]
pub struct ParseClsidError {
/// The clone of string which is not a valid CLSID.
inner: String,
@@ -301,7 +331,7 @@ impl Display for Clsid {
/// Error occurs when given string is not a valid Icon Reference String.
#[derive(Debug, TeError)]
#[error("given string \"{inner}\" is not a valid Icon Reference String")]
#[error(r#"given string "{inner}" is not a valid Icon Reference String"#)]
pub struct ParseIconRefStrError {
/// The clone of string which is not a valid Icon Reference String
inner: String,
@@ -316,11 +346,22 @@ impl ParseIconRefStrError {
}
}
// TODO:
// IconRefStr should process the quote issue for path part in itself,
// rather than fetching its path part and resolve quote issue in another place.
/// The struct representing an Icon Reference String
/// looks like `%SystemRoot%\System32\imageres.dll,-72`.
///
/// As far as I know, the minus token `-` does nothing in this string.
/// The following number is just the index.
///
/// If you have a Icon Reference String,
/// please utilize [FromStr] trait to parse it into this struct.
/// You also can use [IconRefStr::new] with path and index parts to build this struct.
///
/// The [Display] trait this struct implemented always
/// outputs Icon Reference String in string form.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct IconRefStr {
/// The path part of this reference string.
@@ -352,6 +393,12 @@ impl IconRefStr {
/// Get the path part of this reference string.
///
/// This path can be absolute path, relative path or expandable path.
///
/// # Todo
///
/// Currently this return value may be quoted path.
/// You may need manually unquote it.
/// This issue will be resolved in furture.
pub fn get_path(&self) -> &str {
&self.path
}
@@ -373,7 +420,7 @@ impl FromStr for IconRefStr {
fn from_str(s: &str) -> Result<Self, Self::Err> {
static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^([^,@].*),-([0-9]+)$").expect("unexpected bad regex pattern string")
Regex::new(r"^([^,@][^,]*),-?([0-9]+)$").expect("unexpected bad regex pattern string")
});
let caps = RE.captures(s);
if let Some(caps) = caps {
@@ -395,7 +442,7 @@ impl FromStr for IconRefStr {
/// Error occurs when given string is not a valid String Reference String.
#[derive(Debug, TeError)]
#[error("given string \"{inner}\" is not a valid String Reference String")]
#[error(r#"given string "{inner}" is not a valid String Reference String"#)]
pub struct ParseStrRefStrError {
/// The clone of string which is not a valid String Reference String
inner: String,
@@ -410,11 +457,22 @@ impl ParseStrRefStrError {
}
}
// TODO:
// StrRefStr should process the quote issue for path part in itself,
// rather than fetching its path part and resolve quote issue in another place.
/// The struct representing an String Reference String
/// looks like `@%SystemRoot%\System32\shell32.dll,-30596`.
///
/// As far as I know, the minus token `-` does nothing in this string.
/// The following number is just the index.
///
/// If you have a String Reference String,
/// please utilize [FromStr] trait to parse it into this struct.
/// You also can use [StrRefStr::new] with path and index parts to build this struct.
///
/// The [Display] trait this struct implemented always
/// outputs String Reference String in string form.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StrRefStr {
/// The path part of this reference string.
@@ -440,6 +498,12 @@ impl StrRefStr {
/// Get the path part of this reference string.
///
/// This path can be absolute path, relative path or expandable path.
///
/// # Todo
///
/// Currently this return value may be quoted path.
/// You may need manually unquote it.
/// This issue will be resolved in furture.
pub fn get_path(&self) -> &str {
&self.path
}
@@ -461,7 +525,7 @@ impl FromStr for StrRefStr {
fn from_str(s: &str) -> Result<Self, Self::Err> {
static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^@(.+),-([0-9]+)$").expect("unexpected bad regex pattern string")
Regex::new(r"^@([^,]+),-?([0-9]+)$").expect("unexpected bad regex pattern string")
});
let caps = RE.captures(s);
if let Some(caps) = caps {
@@ -504,14 +568,40 @@ pub enum IconSizeKind {
Large,
}
/// The struct representing a loaded icon resource.
/// The struct representing a loaded Win32 icon resource.
///
/// You can use [IconRc::new] or [IconRc::with_ico_file] to create this resource.
///
/// The ownership of loaded Win32 icon resource is managed by this struct,
/// and will be free once this struct was destroyed.
///
/// According to Microsoft manual,
/// Win32 icon resource is program scope resource,
/// so it can be safely used between different threads.
#[derive(Debug)]
pub struct IconRc {
icon: HICON,
}
impl IconRc {
/// Get the icon preset representing generic document.
#[allow(non_snake_case)]
pub fn GENERIC_DOCUMENT(kind: IconSizeKind) -> Result<Self, LoadIconRcError> {
Self::new("shell32.dll", 0, kind)
}
/// Get the icon preset representing generic executable.
#[allow(non_snake_case)]
pub fn GENERIC_APPLICATION(kind: IconSizeKind) -> Result<Self, LoadIconRcError> {
Self::new("imageres.dll", 0, kind)
}
}
impl IconRc {
/// Load icon from executable or `.ico` file.
///
/// Please note that the content of `file` parameter must NOT be
/// expandable string or quoted string.
///
/// If you want to extract icon from `.ico` file, please pass `0` to `index` parameter.
/// Otherwise `index` is the icon resource index located in executable.
@@ -548,16 +638,28 @@ impl IconRc {
Self::new(file, 0, kind)
}
/// Construct this struct in raw Win32 icon handle.
///
/// Please note that after calling this function,
/// the ownership of given Win32 icon handle are managed by this struct.
pub unsafe fn from_raw(hicon: HICON) -> Self {
Self { icon: hicon }
}
pub fn into_raw(self) -> HICON {
self.icon
/// Consume and leak this struct held Win32 icon handle.
///
/// The ownership of this Win32 icon handle will be transfered to the caller of this function.
pub fn into_raw(mut self) -> HICON {
let rv = self.icon;
self.icon = std::ptr::null_mut();
rv
}
}
impl IconRc {
/// Get underlying Win32 icon handle.
///
/// Please note that the ownership of this handle are still in this struct.
pub fn get_icon(&self) -> HICON {
self.icon
}
@@ -575,6 +677,18 @@ impl Drop for IconRc {
}
}
// SAFETY:
// Win32 HICON can be safely moved between different threads naturally.
unsafe impl Send for IconRc {}
// SAFETY:
// When multiple threads calling this struct, there is no conflict between them,
// because this struct only provide HICON itself.
//
// However, the caller should ensure that all calls using this HICON
// follow Win32 thread model.
unsafe impl Sync for IconRc {}
// endregion
// region: String Resource
@@ -596,12 +710,18 @@ pub enum LoadStrRcError {
}
/// The struct representing a loaded string resource.
///
/// You can use [StrRc::new] to create this resource.
#[derive(Debug)]
pub struct StrRc {
inner: String,
}
impl StrRc {
/// Load icon from executable file.
///
/// Please note that the content of `file` parameter must NOT be
/// expandable string or quoted string.
pub fn new(file: &str, index: u32) -> Result<Self, LoadStrRcError> {
use windows_sys::Win32::Foundation::FreeLibrary;
use windows_sys::Win32::System::LibraryLoader::{
@@ -647,10 +767,12 @@ impl StrRc {
}
impl StrRc {
/// Get fetched string resource.
pub fn get_string(&self) -> &str {
&self.inner
}
/// Consume and return fetched string resource.
pub fn into_string(self) -> String {
self.inner
}
@@ -691,21 +813,24 @@ pub enum ExpandEnvVarError {
NoEnvVar,
}
/// The struct representing an Expand String,
/// The struct representing an expandable string,
/// which contain environment variable in string,
/// like `%LOCALAPPDATA%\SomeApp.exe`.
///
/// You can use [ExpandString::new] or [FromStr] trait to create an expandable string,
/// and use [ExpandString::expand] to try expanding it.
///
/// The [Display] trait this struct implemented always
/// outputs original expandable string.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ExpandString {
inner: String,
}
impl ExpandString {
/// Internal shared compiled regex pattern matching Variable,
/// the `%` braced string like `%SystemRoot%`.
const VAR_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"%[a-zA-Z0-9_]+%").expect("unexpected bad regex pattern string")
});
}
/// Internal shared compiled regex pattern matching Variable,
/// the `%` braced string like `%SystemRoot%`.
static WINVAR_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"%[a-zA-Z0-9_]+%").expect("unexpected bad regex pattern string"));
impl ExpandString {
/// Create a new expand string
@@ -714,17 +839,17 @@ impl ExpandString {
}
/// Expand the variables located in this string
/// and produce the final usable string.
pub fn expand_string(&self) -> Result<String, ExpandEnvVarError> {
/// and produce the final string without having any embedded variable syntax.
pub fn expand(&self) -> Result<String, ExpandEnvVarError> {
use windows_sys::Win32::System::Environment::ExpandEnvironmentStringsW;
// Fetch the size of expand result
let source = WideCString::from_str(self.inner.as_str())?;
// The return size is including null terminal.
let size = unsafe { ExpandEnvironmentStringsW(source.as_ptr(), std::ptr::null_mut(), 0) };
if size == 0 {
return Err(ExpandEnvVarError::ExpandFunction);
}
let size_no_nul = size.checked_sub(1).ok_or(ExpandEnvVarError::Underflow)?;
// Allocate buffer for it.
let len: usize = size.try_into()?;
@@ -732,7 +857,7 @@ impl ExpandString {
let mut buffer = vec![0; len];
// Receive result
let size =
unsafe { ExpandEnvironmentStringsW(source.as_ptr(), buffer.as_mut_ptr(), size_no_nul) };
unsafe { ExpandEnvironmentStringsW(source.as_ptr(), buffer.as_mut_ptr(), size) };
if size == 0 {
return Err(ExpandEnvVarError::ExpandFunction);
}
@@ -743,7 +868,7 @@ impl ExpandString {
// If the final string still has environment variable,
// we think we fail to expand it.
if Self::VAR_RE.is_match(rv.as_str()) {
if WINVAR_RE.is_match(rv.as_str()) {
Err(ExpandEnvVarError::NoEnvVar)
} else {
Ok(rv)
@@ -761,7 +886,7 @@ impl FromStr for ExpandString {
type Err = ParseExpandStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if Self::VAR_RE.is_match(s) {
if WINVAR_RE.is_match(s) {
Ok(Self {
inner: s.to_string(),
})
@@ -775,9 +900,15 @@ impl FromStr for ExpandString {
// region: File Name
/// The error occurs when constructing FileName with bad file name.
pub type BadFileNameError = ParseFileNameError;
/// The struct representing a legal Windows file name.
///
/// You can utilize [FileName::new] or [FromStr] trait to create this struct.
///
/// The [Display] trait this struct implemented always
/// outputs the file name in string form.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FileName {
/// The validated legal Windows file name.
@@ -801,7 +932,7 @@ impl FileName {
}
}
/// The error occurs when constructing FileName with bad file name.
/// The error occurs when parsing FileName with bad file name.
#[derive(Debug, TeError)]
#[error("given file name is illegal in Windows")]
pub enum ParseFileNameError {
@@ -848,9 +979,15 @@ impl FromStr for FileName {
// region: Verb
/// The error occurs when constructing Verb with bad verb name.
pub type BadVerbError = ParseVerbError;
/// The struct representing a verb when manipulating file
///
/// You can utilize [Verb::new] or [FromStr] trait to create this struct.
///
/// The [Display] trait this struct implemented always
/// outputs the verb in string form.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Verb {
/// The validated verb name.
@@ -858,9 +995,23 @@ pub struct Verb {
}
impl Verb {
pub const OPEN: LazyLock<Verb> = LazyLock::new(|| Verb::new("open").expect("unexpected bad verb"));
pub const EDIT: LazyLock<Verb> = LazyLock::new(|| Verb::new("edit").expect("unexpected bad verb"));
pub const PLAY: LazyLock<Verb> = LazyLock::new(|| Verb::new("play").expect("unexpected bad verb"));
/// Get the verb representing Open.
#[allow(non_snake_case)]
pub fn OPEN() -> Self {
Verb::new("open").expect("unexpected bad verb")
}
/// Get the verb representing Edit.
#[allow(non_snake_case)]
pub fn EDIT() -> Self {
Verb::new("edit").expect("unexpected bad verb")
}
/// Get the verb representing Play.
#[allow(non_snake_case)]
pub fn PLAY() -> Self {
Verb::new("play").expect("unexpected bad verb")
}
}
impl Verb {
@@ -878,9 +1029,9 @@ impl Verb {
}
}
/// The error occurs when constructing Verb with bad verb name.
/// The error occurs when parsing Verb with bad verb name.
#[derive(Debug, TeError)]
#[error("given verb \"{inner}\" is illegal")]
#[error(r#"given verb "{inner}" is illegal"#)]
pub struct ParseVerbError {
inner: String,
}
@@ -919,50 +1070,106 @@ impl FromStr for Verb {
// region: Command Line
// TODO:
// Introduce CmdArg in future to describe single Windows command line argument.
// And use CmdArg to finish the full functionality of CmdLine.
/// The error occurs when constructing CmdLine with bad arguments.
#[derive(Debug, TeError)]
#[error("given file extension body \"{inner}\" is invalid")]
#[error(r#"given command line argument "{inner}" is invalid"#)]
pub struct BadCmdLineError {
inner: String,
}
impl BadCmdLineError {
// TODO: Remove this dead_code attribute once we need use this error type.
#[allow(dead_code)]
fn new(inner: &str) -> Self {
Self {
inner: inner.to_string(),
}
}
}
/// The struct representing a Windows command line.
///
/// # Note
/// If you have a complete Windows command line expressed in string form,
/// you can utilize [FromStr] trait to create this struct.
/// Or, you can utilize [CmdLine::new] to create this struct with command arguments one by one.
///
/// The [Display] trait this struct implemented always
/// outputs a legal Windows command line in string form,
/// which combine all arguments together.
///
/// # Todo
///
/// This struct currently does nothing for validation for given command line.
/// Because there is no standard for validating this.
/// So I just write this struct as a placeholder for future extension.
///
/// Currently if you are using [FromStr] trait to parse command line,
/// it will simply split it with space as separator.
/// This may cause unexpected behavior such as empty argument or unexpected break in argument.
/// This issue will be resolved in future.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CmdLine {
inner: Vec<String>,
}
impl CmdLine {
/// Create a new command line.
/// Create a new command line with arguments one by one.
///
/// Please note that passed command line argument must be unquoted.
///
/// # Todo
///
/// Currently, there is no unquote restrictions for those passed arguments.
/// These passed argument will be honestly used in full command line.
/// So if your argument require quote, please quote them at first.
/// This issue will be resolved in future.
pub fn new<S: AsRef<str>>(args: &[S]) -> Result<Self, BadCmdLineError> {
Ok(Self { inner: args.iter().map(|s| s.as_ref().to_string()).collect() })
Ok(Self {
inner: args.iter().map(|s| s.as_ref().to_string()).collect(),
})
}
/// Get the full command line.
/// Get the full command line in string form with proper quote.
///
/// # Todo
///
/// Currently this function only simply returns
/// the joined command line arguments with space as separator.
/// There is no guarantee about safe quote.
/// This issue will be resolved in future.
pub fn full(&self) -> String {
self.inner.join(" ")
}
/// Get the iterator of command line arguments.
///
/// Each returned argument are unquoted.
///
/// # Todo
///
/// Currently there is no guarantee about the unquote behavior
/// for returned command line arguments.
/// It simply returns the content of arguments list passed to constructor.
/// This issue will be resolved in future.
pub fn iter(&self) -> impl Iterator<Item = &str> {
self.inner.iter().map(|s| s.as_str())
}
}
/// The error occurs when constructing CmdLine with bad syntax.
/// The error occurs when parsing CmdLine with bad syntax.
#[derive(Debug, TeError)]
#[error("given command line \"{inner}\" is invalid")]
#[error(r#"given command line "{inner}" is invalid"#)]
pub struct ParseCmdLineError {
inner: String,
}
impl ParseCmdLineError {
// TODO: Remove this dead_code attribute once we need use this error type.
#[allow(dead_code)]
fn new(inner: &str) -> Self {
Self {
inner: inner.to_string(),
@@ -981,7 +1188,10 @@ impl FromStr for CmdLine {
fn from_str(s: &str) -> Result<Self, Self::Err> {
// We simply split it by space for rough result.
Ok(Self { inner: s.split(' ').map(|s| s.to_string()).collect() })
Ok(Self {
inner: s.split(' ').map(|s| s.to_string()).collect(),
})
}
}
// endregion
+127 -11
View File
@@ -1,23 +1,22 @@
//! This module extend `winreg` crate to make it more suit for the usage of this crate.
use std::ffi::OsStr;
use std::ops::Deref;
use std::ops::DerefMut;
use thiserror::Error as TeError;
use windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND;
use windows_sys::Win32::System::Registry::REG_SAM_FLAGS;
use winreg::RegKey;
use winreg::enums::RegType;
use winreg::types::FromRegValue;
// region: Extra Operations
/// Get the subkey with given name.
/// Try getting the subkey with given name.
///
/// If error occurs when fetching given subkey, it return `Err(...)`,
/// otherwise, it will return `Ok(Some(...))` if subkey is existing,
/// or `Ok(None)` if there is no suchsub key.
///
/// Comparing with the function provided by winreg,
/// Comparing with the function provided by [winreg],
/// it differ "no such subkey" error and other access error.
pub fn try_open_subkey_with_flags<P: AsRef<OsStr>>(
regkey: &RegKey,
@@ -36,13 +35,13 @@ pub fn try_open_subkey_with_flags<P: AsRef<OsStr>>(
}
}
/// Get the value by given key.
/// Try getting the value by given key.
///
/// If error occurs when fetching given key, it return `Err(...)`,
/// otherwise, it will return `Ok(Some(...))` if key is existing,
/// or `Ok(None)` if there is no such key.
///
/// Comparing with the function provided by winreg,
/// Comparing with the function provided by [winreg],
/// it differ "no such key" error and other access error.
pub fn try_get_value<T: FromRegValue, N: AsRef<OsStr>>(
regkey: &RegKey,
@@ -60,6 +59,113 @@ pub fn try_get_value<T: FromRegValue, N: AsRef<OsStr>>(
}
}
/// Delete all tree of given path of given key anyway.
///
/// This function was invented to fix the shortcoming of [RegKey::delete_subkey_all].
/// This function always delete given path of given key no matter it is existing.
/// Oppositely, [RegKey::delete_subkey_all] will return error if there is no such path.
///
/// Return true if we successfully delete this key,
/// otherwise false indicating there is no such key (already deleted).
pub fn arbitrarily_delete_subkey_all<P: AsRef<OsStr>>(
regkey: &RegKey,
path: P,
) -> std::io::Result<bool> {
match regkey.delete_subkey_all(path) {
Ok(()) => Ok(true),
Err(e) => match e.raw_os_error() {
Some(errno) => match errno as u32 {
ERROR_FILE_NOT_FOUND => Ok(false),
_ => Err(e),
},
_ => Err(e),
},
}
}
/// Delete given value key of given key anyway.
///
/// This function was invented to fix the shortcoming of [RegKey::delete_value].
/// This function always delete given value key of given key no matter it is existing.
/// Oppositely, [RegKey::delete_value] will return error if there is no such value key.
///
/// Return true if we successfully delete this value key,
/// otherwise false indicating there is no such value key (already deleted).
pub fn arbitrarily_delete_value<N: AsRef<OsStr>>(
regkey: &RegKey,
name: N,
) -> std::io::Result<bool> {
match regkey.delete_value(name) {
Ok(()) => Ok(true),
Err(e) => match e.raw_os_error() {
Some(errno) => match errno as u32 {
ERROR_FILE_NOT_FOUND => Ok(false),
_ => Err(e),
},
_ => Err(e),
},
}
}
// TODO:
// Once we support multiple ShellVerb,
// it seems that there is no need to preserve this function.
/// Get the name of only subkey in given key.
///
/// If there is only one subkey in given key, the return value is its name.
/// If there is no any subkey, or has multiple subkeys, return None instead.
/// If error occurs when fetching data, return `Err(_)`.
///
/// This is used as fetching only one ShellVerb.
pub fn get_sole_subkey_name(regkey: &RegKey) -> std::io::Result<Option<String>> {
let mut subkey_enumerator = regkey.enum_keys();
// Get first one.
let rv = match subkey_enumerator.next() {
Some(key) => key?,
None => return Ok(None),
};
// Check whether there is second one.
match subkey_enumerator.next() {
Some(_) => Ok(None),
None => Ok(Some(rv)),
}
}
/// Get the name list of all "string" subkeys in given key.
///
/// This is used in "OpenWithProgIds" subkey.
pub fn get_all_string_subkey_names(regkey: &RegKey) -> std::io::Result<Vec<String>> {
regkey
.enum_values()
.filter_map(|item| match item {
Ok((key, value)) => {
// Check subkey type.
if value.vtype == RegType::REG_SZ {
Some(Ok(key))
} else {
None
}
}
Err(err) => Some(Err(err)),
})
.collect::<Result<Vec<_>, _>>()
}
/// Delete all contents, including values and subkeys of given key.
///
/// Deleting all contents of given key rely on giving a special parameter to [RegKey::delete_subkey_all].
/// This is very dangerous and may be used by accident.
/// So I create this to explicitly indicate this behavior and avoid any mis-type in code.
pub fn clean_all_contents(regkey: &RegKey) -> std::io::Result<()> {
// There is no possibility that this key do not existing,
// because what we are cleaning is self content.
// So directly use delete_subkey_all is okey.
regkey.delete_subkey_all("")
}
// endregion
// region: Blank Path Guard
@@ -76,12 +182,15 @@ impl BlankPathError {
}
/// Check whether given registry path is empty.
/// If it is, return error, otherwise the given path.
/// If it is, return error, otherwise the given path itself.
///
/// Passing empty path for some registry functions is dangerous.
/// Because it will cause unexpected behavior that returning key self, rather than subkey.
/// This is VERY dangerous especially for those registry delete functions.
/// So I create this function to prevent any harmful blank path was passed into registry function.
/// Passing empty path for some [winreg] functions is dangerous, expecially for those registry delete functions.
/// Passing empty path to registry creation function will return themselves,
/// and passing them to registry delete functions will delete all contents of them!
/// So I create this function to prevent any harmful blank path was passed into [winreg] function.
///
/// This function MUST be used for wrapping the value, whose content can not be confirmed at compile time,
/// if that value will be passed to get/set value, or create/delete key functions.
pub fn blank_path_guard<P: AsRef<OsStr>>(path: P) -> std::result::Result<P, BlankPathError> {
if path.as_ref().is_empty() {
Err(BlankPathError::new())
@@ -94,6 +203,11 @@ pub fn blank_path_guard<P: AsRef<OsStr>>(path: P) -> std::result::Result<P, Blan
// region: Expand String
// TODO:
// Re-enable following code when we need to distinguish between REG_SZ and REG_EXPAND_SZ.
/*
/// The struct basically is the alias of String, but make a slight difference with it,
/// to make they are different when use it with String as generic argument.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -161,4 +275,6 @@ impl From<&str> for ExpandString {
}
}
*/
// endregion
+6 -1
View File
@@ -7,7 +7,12 @@
/// It usually means that checking whether current process is running as Administrator.
/// Return true if it is, otherwise false.
///
/// Reference: https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership
/// Reference: <https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership>
///
/// # Panics
///
/// This function will panic if any Windows functions failed unexpectedly.
/// This is a very common check and in theory there is no any possibility that this check will fail.
pub fn has_privilege() -> bool {
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Security::{
+27
View File
@@ -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."
)
)
}
+50 -8
View File
@@ -1,4 +1,4 @@
use std::str::FromStr;
use std::{collections::HashMap, str::FromStr};
use wfassoc::win32::concept::*;
// region: File Extension
@@ -134,6 +134,13 @@ fn test_icon_ref_str() {
r#"%SystemRoot%\System32\imageres.dll,-72"#,
(r#"%SystemRoot%\System32\imageres.dll"#, 72),
);
ok_tester(
r#""D:\Software\Krita\Krita (x64)\shellex\kritafile.ico",0"#,
(
r#""D:\Software\Krita\Krita (x64)\shellex\kritafile.ico""#,
0,
),
);
err_tester(r#"C:\Windows\Cursors\aero_arrow.cur"#);
err_tester(r#"This is my application, OK?"#);
err_tester(r#"@%SystemRoot%\System32\shell32.dll,-30596"#);
@@ -161,6 +168,10 @@ fn test_str_ref_str() {
r#"@%SystemRoot%\System32\shell32.dll,-30596"#,
(r#"%SystemRoot%\System32\shell32.dll"#, 30596),
);
ok_tester(
r#"@%SystemRoot%\System32\shell32.dll,30596"#,
(r#"%SystemRoot%\System32\shell32.dll"#, 30596),
);
err_tester(r#"This is my application, OK?"#);
err_tester(r#"%SystemRoot%\System32\imageres.dll,-72"#);
}
@@ -175,6 +186,9 @@ fn test_str_ref_str() {
#[test]
fn test_icon_rc() {
fn const_tester(icon: Result<IconRc, LoadIconRcError>) {
assert!(icon.is_ok())
}
fn ok_tester(file: &str, index: u32) {
let icon = IconRc::new(file, index, IconSizeKind::Small);
assert!(icon.is_ok())
@@ -184,6 +198,9 @@ fn test_icon_rc() {
assert!(icon.is_err())
}
// Test 2 const value
const_tester(IconRc::GENERIC_APPLICATION(IconSizeKind::Small));
const_tester(IconRc::GENERIC_APPLICATION(IconSizeKind::Small));
// We pick it from "jpegfile" ProgId
ok_tester("imageres.dll", 72);
ok_tester("notepad.exe", 0);
@@ -218,16 +235,36 @@ fn test_str_rc() {
#[test]
fn test_expand_string() {
fn tester(s: &str) {
let rv = ExpandString::new(s);
assert!(rv.is_ok());
let rv = rv.unwrap();
fn tester(fmt: &str, var_name: &str) {
// We first insert variable name into format string to get the final string
let mut vars = HashMap::new();
vars.insert("0".to_string(), var_name.to_string());
let final_string = strfmt::strfmt(fmt, &vars).unwrap();
let rv = rv.expand_string();
assert!(rv.is_ok());
// The we try expanding final string first
let expand_final_string = ExpandString::new(&final_string);
assert!(expand_final_string.is_ok());
let expand_final_string = expand_final_string.unwrap();
let expanded_final_string = expand_final_string.expand();
assert!(expanded_final_string.is_ok());
let expanded_final_string = expanded_final_string.unwrap();
// Then we expand variable name individually
let expand_var_name = ExpandString::new(var_name);
assert!(expand_var_name.is_ok());
let expand_var_name = expand_var_name.unwrap();
let expanded_var_name = expand_var_name.expand();
assert!(expanded_var_name.is_ok());
let expanded_var_name = expanded_var_name.unwrap();
// Finally, we directly insert expanded variable name into format string
// to get the string which can be compared with final string.
vars.insert("0".to_string(), expanded_var_name.clone());
let built_final_string = strfmt::strfmt(fmt, &vars).unwrap();
assert_eq!(expanded_final_string.to_string(), built_final_string);
}
tester(r#"%SystemRoot%\System32\shell32.dll"#);
tester(r#"{0}\System32\shell32.dll"#, "%SystemRoot%");
}
// endregion
@@ -281,12 +318,17 @@ fn test_verb() {
#[test]
fn test_cmd_line() {
// TODO:
// Remove error tester dead_code attribute,
// improve ok tester and add more error tests
// once we finish a complete CmdLine.
fn ok_tester(s: &str) {
let rv = CmdLine::from_str(s);
assert!(rv.is_ok());
// let rv = rv.unwrap();
// assert_eq!(s, rv.full());
}
#[allow(dead_code)]
fn err_tester(s: &str) {
let rv = CmdLine::from_str(s);
assert!(rv.is_err());
+169
View File
@@ -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
+334
View File
@@ -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