Compare commits

...
28 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
72 changed files with 5295 additions and 2475 deletions
+198 -74
View File
@@ -34,7 +34,7 @@ clsid = "{B5291320-FE7C-4069-BF87-A0AC327FCD20}"
# #
# This key is optional. If you don't fill this key, # This key is optional. If you don't fill this key,
# Windows will use your executable embedded Win32 resources for friendly name. # Windows will use your executable embedded Win32 resources for friendly name.
name = "default" name = "self"
# The icon of this program. # The icon of this program.
# #
@@ -42,7 +42,7 @@ name = "default"
# #
# This key is optional. If you don't fill this key, # This key is optional. If you don't fill this key,
# Windows will use the first icon embedded in your executable as icon. # Windows will use the first icon embedded in your executable as icon.
icon = "others" icon = "self"
# The default behavior of your program. # The default behavior of your program.
# #
@@ -69,31 +69,41 @@ behavior = "generic"
# #
# Due to the same use of back-slash in path part, # Due to the same use of back-slash in path part,
# you can utilize TOML "literal strings" syntax to write this value easier. # 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, # 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. # 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... # And more string resources...
jpg = '@C:\path\to\ppic.exe,-1011' jpeg = 'JPEG Image'
jfif = '@C:\path\to\ppic.exe,-1050' gif = 'GIF Animation Image'
gif = '@C:\path\to\ppic.exe,-1012' png = 'PNG Image'
bmp = '@C:\path\to\ppic.exe,-1013' svg = 'Scalable Vector Graphics'
png = '@C:\path\to\ppic.exe,-1014' webp = 'WebP Image'
ico = '@C:\path\to\ppic.exe,-1015' avif = 'AV1 Image File'
jpeg = '@C:\path\to\ppic.exe,-1016' ico = 'Windows Icon Image'
tif = '@C:\path\to\ppic.exe,-1017' icns = 'Apple Icon Image'
tiff = '@C:\path\to\ppic.exe,-1018' psd = 'Photoshop Document'
webp = '@C:\path\to\ppic.exe,-1019' kra = 'Krita Document'
avif = '@C:\path\to\ppic.exe,-1020' xcf = 'GIMP Document'
svg = '@C:\path\to\ppic.exe,-1021' tga = 'TARGA Image'
kra = '@C:\path\to\ppic.exe,-1022' tif = 'Tagged Image File Format'
xcf = '@C:\path\to\ppic.exe,-1023' bmp = 'Bitmap'
qoi = '@C:\path\to\ppic.exe,-1024' wbmp = 'Wireless Bitmap'
apng = '@C:\path\to\ppic.exe,-1025' dds = 'DirectDraw Surface'
exr = '@C:\path\to\ppic.exe,-1026' hdr = 'High Dynamic Range Image'
tga = '@C:\path\to\ppic.exe,-1027' 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 ======== # ======== Icon Resources ========
@@ -113,18 +123,17 @@ tga = '@C:\path\to\ppic.exe,-1027'
# #
# Due to the same use of back-slash in path part, # Due to the same use of back-slash in path part,
# you can utilize TOML "literal strings" syntax to write this value easier. # 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. # 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... # Add more icon resources...
svg = 'C:\path\to\ppic.exe,-2' gif = 'C:\path\to\ppic\icons\gif.ico'
gif = 'C:\path\to\ppic.exe,-3' jpeg = 'C:\path\to\ppic\icons\jpeg.ico'
jpg = 'C:\path\to\ppic.exe,-4' png = 'C:\path\to\ppic\icons\png.ico'
webp = 'C:\path\to\ppic.exe,-5' psd = 'C:\path\to\ppic\icons\psd.ico'
avif = 'C:\path\to\ppic.exe,-6' svg = 'C:\path\to\ppic\icons\svg.ico'
others = 'C:\path\to\ppic.exe,-61'
# ======== Behaviors Collection ======== # ======== Behaviors Collection ========
@@ -162,76 +171,191 @@ generic = '"C:\path\to\ppic.exe" "%1"'
[exts.jpg] [exts.jpg]
# First, we specify its name shown in Windows Explorer. # First, we specify its name shown in Windows Explorer.
# The value of this field is the token declared above in "strs" list. # The value of this field is the token declared above in "strs" list.
name = "jpg" name = "jpeg"
# Then, speficy its associated icon. # Then, speficy its associated icon.
# The value of this field is the token declared above in "icons" list. # 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. # 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. # The value of this field is the token declared above in "behaviors" list.
behavior = "generic" behavior = "generic"
# And more file extensions... # And more file extensions...
[exts.jfif] [exts.jpeg]
name = "jfif" name = "jpeg"
icon = "jpg" icon = "jpeg"
behavior = "generic" behavior = "generic"
[exts.jfif]
name = "jpeg"
icon = "jpeg"
behavior = "generic"
[exts.gif] [exts.gif]
name = "gif" name = "gif"
icon = "gif" icon = "gif"
behavior = "generic" behavior = "generic"
[exts.bmp]
name = "bmp"
icon = "others"
behavior = "generic"
[exts.png] [exts.png]
name = "png" name = "png"
icon = "png" icon = "png"
behavior = "generic" 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] [exts.svg]
name = "svg" name = "svg"
icon = "svg" icon = "svg"
behavior = "generic" behavior = "generic"
[exts.kra]
name = "kra" [exts.webp]
icon = "others" name = "webp"
behavior = "generic" icon = "generic"
[exts.xcf]
name = "xcf"
icon = "others"
behavior = "generic" behavior = "generic"
[exts.avif] [exts.avif]
name = "avif" name = "avif"
icon = "avif" icon = "generic"
behavior = "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] [exts.qoi]
name = "qoi" name = "qoi"
icon = "others" icon = "generic"
behavior = "generic" behavior = "generic"
[exts.apng]
name = "apng" [exts.ras]
icon = "png" name = "ras"
icon = "generic"
behavior = "generic" behavior = "generic"
[exts.exr] [exts.sun]
name = "exr" name = "ras"
icon = "others" 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" 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.
@@ -5,7 +5,7 @@
# This module requires the user to set wfassoc_ROOT to the installation # This module requires the user to set wfassoc_ROOT to the installation
# directory of wfassoc. The directory structure under wfassoc_ROOT must be: # directory of wfassoc. The directory structure under wfassoc_ROOT must be:
# bin/ - contains wfassoc_cdylib.dll # bin/ - contains wfassoc_cdylib.dll
# include/ - contains wfassoc.h # include/ - contains wfassoc.h and wfassoc++.h
# lib/ - contains wfassoc_cdylib.dll.lib (import library) # lib/ - contains wfassoc_cdylib.dll.lib (import library)
# #
# This module defines the following variables: # This module defines the following variables:
@@ -36,7 +36,7 @@ set(wfassoc_LIB_DIR ${wfassoc_ROOT}/lib)
set(wfassoc_BIN_DIR ${wfassoc_ROOT}/bin) set(wfassoc_BIN_DIR ${wfassoc_ROOT}/bin)
# Find header files # Find header files
if(EXISTS ${wfassoc_INCLUDE_DIR}/wfassoc.h) if(EXISTS ${wfassoc_INCLUDE_DIR}/wfassoc.h AND EXISTS ${wfassoc_INCLUDE_DIR}/wfassoc++.h)
set(wfassoc_INCLUDE_DIRS ${wfassoc_INCLUDE_DIR}) set(wfassoc_INCLUDE_DIRS ${wfassoc_INCLUDE_DIR})
else() else()
message(SEND_ERROR "Missing wfassoc header files in ${wfassoc_INCLUDE_DIR}") message(SEND_ERROR "Missing wfassoc header files in ${wfassoc_INCLUDE_DIR}")
+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>
+1 -1
View File
@@ -1,4 +1,4 @@
# 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. 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.
+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_
-117
View File
@@ -1,117 +0,0 @@
#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
using CStyleString = const char*;
using Token = uint64_t;
using HICON = void*;
#else // __cplusplus
typedef const char *CStyleString;
typedef uint64_t Token;
typedef void *HICON;
#endif // __cplusplus
#ifdef __cplusplus
enum class Scope : uint32_t {
User = 0u,
System = 1u
};
enum class View : uint32_t {
User = 0u,
System = 1u,
Hybrid = 2u
};
#else // __cplusplus
typedef uint32_t Scope;
static const Scope SCOPE_USER = 0u;
static const Scope SCOPE_SYSTEM = 1u;
typedef uint32_t View;
static const View VIEW_USER = 0u;
static const View VIEW_SYSTEM = 1u;
static const View VIEW_HYBRID = 2u;
#endif // __cplusplus
#ifdef __cplusplus
namespace wfassoc {
extern "C" {
#endif // __cplusplus
bool WFStartup(void);
bool WFShutdown(void);
CStyleString WFGetLastError(void);
bool WFHasPrivilege(void);
Token WFInvalidToken(void);
bool WFSchemaCreate(Token *out_schema);
bool WFSchemaDestroy(Token in_schema);
bool WFSchemaSetIdentifier(Token in_schema, CStyleString in_value);
bool WFSchemaSetPath(Token in_schema, CStyleString in_value);
bool WFSchemaSetClsid(Token in_schema, CStyleString in_value);
bool WFSchemaSetName(Token in_schema, CStyleString in_value);
bool WFSchemaSetIcon(Token in_schema, CStyleString in_value);
bool WFSchemaSetBehavior(Token in_schema, CStyleString in_value);
bool WFSchemaAddStr(Token in_schema, CStyleString in_name, CStyleString in_value);
bool WFSchemaAddIcon(Token in_schema, CStyleString in_name, CStyleString in_value);
bool WFSchemaAddBehavior(Token in_schema, CStyleString in_name, CStyleString in_value);
bool WFSchemaAddExt(Token in_schema,
CStyleString in_ext,
CStyleString in_ext_name,
CStyleString in_ext_icon,
CStyleString in_ext_behavior);
bool WFProgramCreate(Token in_schema, Token *out_program);
bool WFProgramDestroy(Token in_program);
bool WFProgramRegister(Token in_program, Scope in_scope);
bool WFProgramUnregister(Token in_program, Scope in_scope);
bool WFProgramIsRegistered(Token in_program, Scope in_scope, bool *out_is_registered);
bool WFProgramLinkExt(Token in_program, Scope in_scope, size_t in_index);
bool WFProgramUnlinkExt(Token in_program, Scope in_scope, size_t in_index);
bool WFProgramQueryExt(Token in_program, View in_view, size_t in_index, Token *out_ext_status);
bool WFExtStatusDestroy(Token in_ext_status);
bool WFExtStatusGetName(Token in_ext_status, CStyleString *out_name);
bool WFExtStatusGetIcon(Token in_ext_status, HICON *out_icon);
#ifdef __cplusplus
} // extern "C"
} // namespace wfassoc
#endif // __cplusplus
#endif // __WFASSOC_H__
+2 -4
View File
@@ -9,11 +9,9 @@
//! The only thing that outer programs should note is that this string is volatile, //! 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. //! once they get it, they must dupliate it immediately before any futher calling to this dynamic library.
use std::cell::RefCell; use std::cell::RefCell;
use std::ffi::{CStr, CString, c_char}; use std::ffi::{CStr, CString};
use thiserror::Error as TeError; use thiserror::Error as TeError;
use crate::ffi_types::CStyleString;
/// The type representing the raw pointer to immutable C-style NUL-terminated string.
pub type CStyleString = *const c_char;
// region: Error // region: Error
+8 -4
View File
@@ -1,11 +1,15 @@
//! The module including all FFI types used by this crate, except string type. //! The module including all FFI types used by this crate, except string type.
//! For string type, see also [crate::cstr_ffi]. //! For string type, see also [crate::cstr_ffi].
use std::ffi::c_void; use std::ffi::{c_void, c_char};
use num_enum::TryFromPrimitive; use num_enum::TryFromPrimitive;
// region: File Extension Index // 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; pub const INVALID_INDEX: usize = usize::MAX;
// endregion // endregion
@@ -18,12 +22,12 @@ pub const INVALID_INDEX: usize = usize::MAX;
/// However, I don't want to add it as this crate's dependency, /// However, I don't want to add it as this crate's dependency,
/// because I don't use anything within it except this type. /// because I don't use anything within it except this type.
/// So I check Microsoft document, re-define it in there for this crate. /// 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 /// Reference: <https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types>
pub type HICON = *mut c_void; pub type HICON = *mut c_void;
/// The invalid value of Win32 HICON handle. /// The invalid value of Win32 HICON handle.
/// ///
/// The same reason like [HICON] to re-define it in there. /// The same reason like [WFHICON] to re-define it in there.
pub const INVALID_HICON: HICON = std::ptr::null_mut(); pub const INVALID_HICON: HICON = std::ptr::null_mut();
// endregion // endregion
+208 -193
View File
@@ -14,27 +14,27 @@ use wfassoc::highlevel::{Program, Schema};
#[derive(Debug, TeError)] #[derive(Debug, TeError)]
enum Error { enum Error {
/// Error when operating Schema. /// Error when operating Schema.
#[error("{0}")] #[error("wfassoc error: {0}")]
Schema(#[from] wfassoc::highlevel::SchemaError), Schema(#[from] wfassoc::highlevel::SchemaError),
/// Error when parsing Schema into Program. /// Error when parsing Schema into Program.
#[error("{0}")] #[error("wfassoc error: {0}")]
ParseProgram(#[from] wfassoc::highlevel::ParseProgramError), ParseProgram(#[from] wfassoc::highlevel::ParseProgramError),
/// Error when operating Program. /// Error when operating Program.
#[error("{0}")] #[error("wfassoc error: {0}")]
Program(#[from] wfassoc::highlevel::ProgramError), Program(#[from] wfassoc::highlevel::ProgramError),
/// Error when manipulating with C-style string. /// Error when manipulating with C-style string.
#[error("{0}")] #[error("C-Style string FFI error: {0}")]
CStrFfi(#[from] cstr_ffi::Error), CStrFfi(#[from] cstr_ffi::Error),
/// Error when manipulating with object pool. /// Error when manipulating with object pool.
#[error("{0}")] #[error("object pool error: {0}")]
ObjectPool(#[from] object_pool::Error), ObjectPool(#[from] object_pool::Error),
/// Error occurs when checking enum value /// Error occurs when checking enum value
#[error("")] #[error("the enumeration value provided to FFI function is out of its range")]
EnumOutOfRange, EnumOutOfRange,
/// Error when manipulating with poison RwLock /// Error when manipulating with poison RwLock
#[error("RwLock is poisoning")] #[error("concurrency error: RwLock is poisonous")]
PoisonRwLock, PoisonRwLock,
} }
@@ -191,19 +191,22 @@ static PROGRAM_POOL: LazyLock<RwLock<ObjectPool<Program>>> =
static EXT_STATUS_POOL: LazyLock<RwLock<ObjectPool<wfassoc::highlevel::ProgramExtStatus>>> = static EXT_STATUS_POOL: LazyLock<RwLock<ObjectPool<wfassoc::highlevel::ProgramExtStatus>>> =
LazyLock::new(|| RwLock::new(ObjectPool::new())); LazyLock::new(|| RwLock::new(ObjectPool::new()));
static ICON_RC_POOL: LazyLock<RwLock<ObjectPool<wfassoc::win32::concept::IconRc>>> = static SELF_EXT_STATUS_POOL: LazyLock<
LazyLock::new(|| RwLock::new(ObjectPool::new())); RwLock<ObjectPool<wfassoc::highlevel::ProgramSelfExtStatus>>,
> = LazyLock::new(|| RwLock::new(ObjectPool::new()));
static EXT_POOL: LazyLock<RwLock<ObjectPool<wfassoc::win32::concept::Ext>>> = static ICON_RC_POOL: LazyLock<RwLock<ObjectPool<wfassoc::win32::concept::IconRc>>> =
LazyLock::new(|| RwLock::new(ObjectPool::new())); LazyLock::new(|| RwLock::new(ObjectPool::new()));
// endregion // endregion
// region: Exposed Types // region: Exposed Types
pub use cstr_ffi::CStyleString; pub use ffi_types::{
pub use ffi_types::{HICON, Scope, View}; CStyleString as WFCString, HICON as WFHICON, Scope as WFScope, View as WFView,
pub use object_pool::Token; };
pub use ffi_types::{INVALID_HICON as WF_INVALID_HICON, INVALID_INDEX as WF_INVALID_INDEX};
pub use object_pool::Token as WFToken;
// endregion // endregion
@@ -218,8 +221,8 @@ pub extern "C" fn WFStartup() -> bool {
let _pool = pull_writer!(SCHEMA_POOL)?; let _pool = pull_writer!(SCHEMA_POOL)?;
let _pool = pull_writer!(PROGRAM_POOL)?; let _pool = pull_writer!(PROGRAM_POOL)?;
let _pool = pull_writer!(EXT_STATUS_POOL)?; let _pool = pull_writer!(EXT_STATUS_POOL)?;
let _pool = pull_writer!(SELF_EXT_STATUS_POOL)?;
let _pool = pull_writer!(ICON_RC_POOL)?; let _pool = pull_writer!(ICON_RC_POOL)?;
let _pool = pull_writer!(EXT_POOL)?;
Ok(()) Ok(())
}) })
} }
@@ -234,16 +237,16 @@ pub extern "C" fn WFShutdown() -> bool {
pool.clear(); pool.clear();
let mut pool = pull_writer!(EXT_STATUS_POOL)?; let mut pool = pull_writer!(EXT_STATUS_POOL)?;
pool.clear(); pool.clear();
let mut pool = pull_writer!(ICON_RC_POOL)?; let mut pool = pull_writer!(SELF_EXT_STATUS_POOL)?;
pool.clear(); pool.clear();
let mut pool = pull_writer!(EXT_POOL)?; let mut pool = pull_writer!(ICON_RC_POOL)?;
pool.clear(); pool.clear();
Ok(()) Ok(())
}) })
} }
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFGetLastError() -> CStyleString { pub extern "C" fn WFGetLastError() -> WFCString {
last_error::get_last_error() last_error::get_last_error()
} }
@@ -253,7 +256,7 @@ pub extern "C" fn WFHasPrivilege() -> bool {
} }
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFInvalidToken() -> Token { pub extern "C" fn WFInvalidToken() -> WFToken {
object_pool::invalid_token() object_pool::invalid_token()
} }
@@ -262,16 +265,16 @@ pub extern "C" fn WFInvalidToken() -> Token {
// region: Schema // region: Schema
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFSchemaCreate(out_schema: out_param_ty!(Token)) -> bool { pub extern "C" fn WFSchemaCreate(out_schema: out_param_ty!(WFToken)) -> bool {
cffi_wrapper!(|| -> (out_schema: Token) { cffi_wrapper!(|| -> (out_schema: WFToken) {
let mut pool = pull_writer!(SCHEMA_POOL)?; let mut pool = pull_writer!(SCHEMA_POOL)?;
Ok(pool.allocate(Schema::new())?) Ok(pool.allocate(Schema::new())?)
}) })
} }
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFSchemaDestroy(in_schema: in_param_ty!(Token)) -> bool { pub extern "C" fn WFSchemaDestroy(in_schema: in_param_ty!(WFToken)) -> bool {
cffi_wrapper!(|in_schema: Token| { cffi_wrapper!(|in_schema: WFToken| {
let mut pool = pull_writer!(SCHEMA_POOL)?; let mut pool = pull_writer!(SCHEMA_POOL)?;
Ok(pool.free(in_schema)?) Ok(pool.free(in_schema)?)
}) })
@@ -279,10 +282,10 @@ pub extern "C" fn WFSchemaDestroy(in_schema: in_param_ty!(Token)) -> bool {
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFSchemaSetIdentifier( pub extern "C" fn WFSchemaSetIdentifier(
in_schema: in_param_ty!(Token), in_schema: in_param_ty!(WFToken),
in_value: in_param_ty!(CStyleString), in_value: in_param_ty!(WFCString),
) -> bool { ) -> bool {
cffi_wrapper!(|in_schema: Token, in_value: CStyleString| { cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?; let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?; let schema = pool.get_mut(in_schema)?;
schema.set_identifier(cstr_ffi::parse_ffi_string(in_value)?); schema.set_identifier(cstr_ffi::parse_ffi_string(in_value)?);
@@ -292,10 +295,10 @@ pub extern "C" fn WFSchemaSetIdentifier(
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFSchemaSetPath( pub extern "C" fn WFSchemaSetPath(
in_schema: in_param_ty!(Token), in_schema: in_param_ty!(WFToken),
in_value: in_param_ty!(CStyleString), in_value: in_param_ty!(WFCString),
) -> bool { ) -> bool {
cffi_wrapper!(|in_schema: Token, in_value: CStyleString| { cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?; let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?; let schema = pool.get_mut(in_schema)?;
schema.set_path(cstr_ffi::parse_ffi_string(in_value)?); schema.set_path(cstr_ffi::parse_ffi_string(in_value)?);
@@ -305,10 +308,10 @@ pub extern "C" fn WFSchemaSetPath(
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFSchemaSetClsid( pub extern "C" fn WFSchemaSetClsid(
in_schema: in_param_ty!(Token), in_schema: in_param_ty!(WFToken),
in_value: in_param_ty!(CStyleString), in_value: in_param_ty!(WFCString),
) -> bool { ) -> bool {
cffi_wrapper!(|in_schema: Token, in_value: CStyleString| { cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?; let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?; let schema = pool.get_mut(in_schema)?;
schema.set_clsid(cstr_ffi::parse_ffi_string(in_value)?); schema.set_clsid(cstr_ffi::parse_ffi_string(in_value)?);
@@ -318,10 +321,10 @@ pub extern "C" fn WFSchemaSetClsid(
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFSchemaSetName( pub extern "C" fn WFSchemaSetName(
in_schema: in_param_ty!(Token), in_schema: in_param_ty!(WFToken),
in_value: in_param_ty!(CStyleString), in_value: in_param_ty!(WFCString),
) -> bool { ) -> bool {
cffi_wrapper!(|in_schema: Token, in_value: CStyleString| { cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?; let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?; let schema = pool.get_mut(in_schema)?;
@@ -337,10 +340,10 @@ pub extern "C" fn WFSchemaSetName(
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFSchemaSetIcon( pub extern "C" fn WFSchemaSetIcon(
in_schema: in_param_ty!(Token), in_schema: in_param_ty!(WFToken),
in_value: in_param_ty!(CStyleString), in_value: in_param_ty!(WFCString),
) -> bool { ) -> bool {
cffi_wrapper!(|in_schema: Token, in_value: CStyleString| { cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?; let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?; let schema = pool.get_mut(in_schema)?;
@@ -356,10 +359,10 @@ pub extern "C" fn WFSchemaSetIcon(
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFSchemaSetBehavior( pub extern "C" fn WFSchemaSetBehavior(
in_schema: in_param_ty!(Token), in_schema: in_param_ty!(WFToken),
in_value: in_param_ty!(CStyleString), in_value: in_param_ty!(WFCString),
) -> bool { ) -> bool {
cffi_wrapper!(|in_schema: Token, in_value: CStyleString| { cffi_wrapper!(|in_schema: WFToken, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?; let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?; let schema = pool.get_mut(in_schema)?;
@@ -375,12 +378,12 @@ pub extern "C" fn WFSchemaSetBehavior(
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFSchemaAddStr( pub extern "C" fn WFSchemaAddStr(
in_schema: in_param_ty!(Token), in_schema: in_param_ty!(WFToken),
in_name: in_param_ty!(CStyleString), in_name: in_param_ty!(WFCString),
in_value: in_param_ty!(CStyleString), in_value: in_param_ty!(WFCString),
) -> bool { ) -> bool {
cffi_wrapper!( cffi_wrapper!(
|in_schema: Token, in_name: CStyleString, in_value: CStyleString| { |in_schema: WFToken, in_name: WFCString, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?; let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?; let schema = pool.get_mut(in_schema)?;
schema.add_str( schema.add_str(
@@ -394,12 +397,12 @@ pub extern "C" fn WFSchemaAddStr(
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFSchemaAddIcon( pub extern "C" fn WFSchemaAddIcon(
in_schema: in_param_ty!(Token), in_schema: in_param_ty!(WFToken),
in_name: in_param_ty!(CStyleString), in_name: in_param_ty!(WFCString),
in_value: in_param_ty!(CStyleString), in_value: in_param_ty!(WFCString),
) -> bool { ) -> bool {
cffi_wrapper!( cffi_wrapper!(
|in_schema: Token, in_name: CStyleString, in_value: CStyleString| { |in_schema: WFToken, in_name: WFCString, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?; let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?; let schema = pool.get_mut(in_schema)?;
schema.add_icon( schema.add_icon(
@@ -413,12 +416,12 @@ pub extern "C" fn WFSchemaAddIcon(
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFSchemaAddBehavior( pub extern "C" fn WFSchemaAddBehavior(
in_schema: in_param_ty!(Token), in_schema: in_param_ty!(WFToken),
in_name: in_param_ty!(CStyleString), in_name: in_param_ty!(WFCString),
in_value: in_param_ty!(CStyleString), in_value: in_param_ty!(WFCString),
) -> bool { ) -> bool {
cffi_wrapper!( cffi_wrapper!(
|in_schema: Token, in_name: CStyleString, in_value: CStyleString| { |in_schema: WFToken, in_name: WFCString, in_value: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?; let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?; let schema = pool.get_mut(in_schema)?;
schema.add_behavior( schema.add_behavior(
@@ -432,17 +435,17 @@ pub extern "C" fn WFSchemaAddBehavior(
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFSchemaAddExt( pub extern "C" fn WFSchemaAddExt(
in_schema: in_param_ty!(Token), in_schema: in_param_ty!(WFToken),
in_ext: in_param_ty!(CStyleString), in_ext: in_param_ty!(WFCString),
in_ext_name: in_param_ty!(CStyleString), in_ext_name: in_param_ty!(WFCString),
in_ext_icon: in_param_ty!(CStyleString), in_ext_icon: in_param_ty!(WFCString),
in_ext_behavior: in_param_ty!(CStyleString), in_ext_behavior: in_param_ty!(WFCString),
) -> bool { ) -> bool {
cffi_wrapper!(|in_schema: Token, cffi_wrapper!(|in_schema: WFToken,
in_ext: CStyleString, in_ext: WFCString,
in_ext_name: CStyleString, in_ext_name: WFCString,
in_ext_icon: CStyleString, in_ext_icon: WFCString,
in_ext_behavior: CStyleString| { in_ext_behavior: WFCString| {
let mut pool = pull_writer!(SCHEMA_POOL)?; let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.get_mut(in_schema)?; let schema = pool.get_mut(in_schema)?;
schema.add_ext( schema.add_ext(
@@ -461,10 +464,10 @@ pub extern "C" fn WFSchemaAddExt(
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFProgramCreate( pub extern "C" fn WFProgramCreate(
in_schema: in_param_ty!(Token), in_schema: in_param_ty!(WFToken),
out_program: out_param_ty!(Token), out_program: out_param_ty!(WFToken),
) -> bool { ) -> bool {
cffi_wrapper!(|in_schema: Token| -> (out_program: Token) { cffi_wrapper!(|in_schema: WFToken| -> (out_program: WFToken) {
let mut pool = pull_writer!(SCHEMA_POOL)?; let mut pool = pull_writer!(SCHEMA_POOL)?;
let schema = pool.pop(in_schema)?; let schema = pool.pop(in_schema)?;
@@ -475,8 +478,8 @@ pub extern "C" fn WFProgramCreate(
} }
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFProgramDestroy(in_program: in_param_ty!(Token)) -> bool { pub extern "C" fn WFProgramDestroy(in_program: in_param_ty!(WFToken)) -> bool {
cffi_wrapper!(|in_program: Token| { cffi_wrapper!(|in_program: WFToken| {
let mut pool = pull_writer!(PROGRAM_POOL)?; let mut pool = pull_writer!(PROGRAM_POOL)?;
Ok(pool.free(in_program)?) Ok(pool.free(in_program)?)
}) })
@@ -484,106 +487,91 @@ pub extern "C" fn WFProgramDestroy(in_program: in_param_ty!(Token)) -> bool {
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFProgramResolveName( pub extern "C" fn WFProgramResolveName(
in_program: in_param_ty!(Token), in_program: in_param_ty!(WFToken),
out_name: out_param_ty!(CStyleString), out_name: out_param_ty!(WFCString),
) -> bool { ) -> bool {
cffi_wrapper!(|in_program: Token| -> (out_name: CStyleString) { cffi_wrapper!(|in_program: WFToken| -> (out_name: WFCString) {
let mut pool = pull_writer!(PROGRAM_POOL)?; let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?; let program = pool.get_mut(in_program)?;
let name = match program.resolve_name()? { let name = program.resolve_name()?;
Some(name) => { cstr_ffi::set_ffi_string(&name)?;
cstr_ffi::set_ffi_string(&name)?; Ok(cstr_ffi::get_ffi_string())
cstr_ffi::get_ffi_string()
},
None => {
std::ptr::null()
},
};
Ok(name)
}) })
} }
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFProgramResolveIcon( pub extern "C" fn WFProgramResolveIcon(
in_program: in_param_ty!(Token), in_program: in_param_ty!(WFToken),
out_icon_rc: out_param_ty!(Token), out_icon_rc: out_param_ty!(WFToken),
) -> bool { ) -> bool {
cffi_wrapper!(|in_program: Token| -> (out_icon_rc: Token) { cffi_wrapper!(|in_program: WFToken| -> (out_icon_rc: WFToken) {
let mut pool = pull_writer!(PROGRAM_POOL)?; let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?; let program = pool.get_mut(in_program)?;
let icon = program.resolve_icon()?; let icon = program.resolve_icon()?;
let token = match icon { let mut pool = pull_writer!(ICON_RC_POOL)?;
Some(icon) => { Ok(pool.allocate(icon)?)
let mut pool = pull_writer!(ICON_RC_POOL)?;
pool.allocate(icon)?
},
None => {
object_pool::invalid_token()
},
};
Ok(token)
}) })
} }
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFProgramExtsLen( pub extern "C" fn WFProgramExtsLen(
in_program: in_param_ty!(Token), in_program: in_param_ty!(WFToken),
out_len: out_param_ty!(usize), out_len: out_param_ty!(usize),
) -> bool { ) -> bool {
cffi_wrapper!(|in_program: Token| -> (out_len: usize) { cffi_wrapper!(|in_program: WFToken| -> (out_len: usize) {
let mut pool = pull_writer!(PROGRAM_POOL)?; let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?; let program = pool.get_mut(in_program)?;
Ok(program.exts_len()) Ok(program.exts_len())
}) })
} }
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramGetExt(
in_program: in_param_ty!(Token),
in_index: in_param_ty!(usize),
out_ext: out_param_ty!(Token),
) -> bool {
cffi_wrapper!(|in_program: Token, in_index: usize| -> (out_ext: Token) {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
let ext = program.get_ext(in_index)?;
let mut pool = pull_writer!(EXT_POOL)?;
let token = pool.allocate(ext.clone())?;
Ok(token)
})
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFProgramFindExt( pub extern "C" fn WFProgramFindExt(
in_program: in_param_ty!(Token), in_program: in_param_ty!(WFToken),
in_body: in_param_ty!(CStyleString), in_body: in_param_ty!(WFCString),
out_index: out_param_ty!(usize), out_index: out_param_ty!(usize),
) -> bool { ) -> bool {
cffi_wrapper!(|in_program: Token, in_body: CStyleString| -> (out_index: usize) { cffi_wrapper!(|in_program: WFToken, in_body: WFCString| -> (out_index: usize) {
let mut pool = pull_writer!(PROGRAM_POOL)?; let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?; let program = pool.get_mut(in_program)?;
let body = cstr_ffi::parse_ffi_string(in_body)?; let body = cstr_ffi::parse_ffi_string(in_body)?;
let index = match program.find_ext(body) { let index = match program.find_ext(body) {
Some(index) => index, Some(index) => index,
None => ffi_types::INVALID_INDEX, None => WF_INVALID_INDEX,
}; };
Ok(index) Ok(index)
}) })
} }
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFProgramRegister( pub extern "C" fn WFProgramResolveExt(
in_program: in_param_ty!(Token), in_program: in_param_ty!(WFToken),
in_scope: in_param_ty!(u32), in_index: in_param_ty!(usize),
out_self_ext_status: out_param_ty!(WFToken),
) -> bool { ) -> bool {
cffi_wrapper!(|in_program: Token, in_scope: u32| { cffi_wrapper!(|in_program: WFToken, in_index: usize| -> (out_self_ext_status: WFToken) {
let mut pool = pull_writer!(PROGRAM_POOL)?; let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?; let program = pool.get_mut(in_program)?;
let scope = resolve_enum!(Scope, in_scope)?;
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())?; program.register(scope.into())?;
Ok(()) Ok(())
}) })
@@ -591,13 +579,13 @@ pub extern "C" fn WFProgramRegister(
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFProgramUnregister( pub extern "C" fn WFProgramUnregister(
in_program: in_param_ty!(Token), in_program: in_param_ty!(WFToken),
in_scope: in_param_ty!(u32), in_scope: in_param_ty!(u32),
) -> bool { ) -> bool {
cffi_wrapper!(|in_program: Token, in_scope: u32| { cffi_wrapper!(|in_program: WFToken, in_scope: u32| {
let mut pool = pull_writer!(PROGRAM_POOL)?; let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?; let program = pool.get_mut(in_program)?;
let scope = resolve_enum!(Scope, in_scope)?; let scope = resolve_enum!(WFScope, in_scope)?;
program.unregister(scope.into())?; program.unregister(scope.into())?;
Ok(()) Ok(())
}) })
@@ -605,28 +593,28 @@ pub extern "C" fn WFProgramUnregister(
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFProgramIsRegistered( pub extern "C" fn WFProgramIsRegistered(
in_program: in_param_ty!(Token), in_program: in_param_ty!(WFToken),
in_scope: in_param_ty!(u32), in_scope: in_param_ty!(u32),
out_is_registered: out_param_ty!(bool), out_is_registered: out_param_ty!(bool),
) -> bool { ) -> bool {
cffi_wrapper!(|in_program: Token, in_scope: u32| -> (out_is_registered: bool) { cffi_wrapper!(|in_program: WFToken, in_scope: u32| -> (out_is_registered: bool) {
let pool = pull_reader!(PROGRAM_POOL)?; let pool = pull_reader!(PROGRAM_POOL)?;
let program = pool.get(in_program)?; let program = pool.get(in_program)?;
let scope = resolve_enum!(Scope, in_scope)?; let scope = resolve_enum!(WFScope, in_scope)?;
Ok(program.is_registered(scope.into())?) Ok(program.is_registered(scope.into())?)
}) })
} }
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFProgramLinkExt( pub extern "C" fn WFProgramLinkExt(
in_program: in_param_ty!(Token), in_program: in_param_ty!(WFToken),
in_scope: in_param_ty!(u32), in_scope: in_param_ty!(u32),
in_index: in_param_ty!(usize), in_index: in_param_ty!(usize),
) -> bool { ) -> bool {
cffi_wrapper!(|in_program: Token, in_scope: u32, in_index: usize| { cffi_wrapper!(|in_program: WFToken, in_scope: u32, in_index: usize| {
let mut pool = pull_writer!(PROGRAM_POOL)?; let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?; let program = pool.get_mut(in_program)?;
let scope = resolve_enum!(Scope, in_scope)?; let scope = resolve_enum!(WFScope, in_scope)?;
program.link_ext(scope.into(), in_index)?; program.link_ext(scope.into(), in_index)?;
Ok(()) Ok(())
}) })
@@ -634,14 +622,14 @@ pub extern "C" fn WFProgramLinkExt(
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFProgramUnlinkExt( pub extern "C" fn WFProgramUnlinkExt(
in_program: in_param_ty!(Token), in_program: in_param_ty!(WFToken),
in_scope: in_param_ty!(u32), in_scope: in_param_ty!(u32),
in_index: in_param_ty!(usize), in_index: in_param_ty!(usize),
) -> bool { ) -> bool {
cffi_wrapper!(|in_program: Token, in_scope: u32, in_index: usize| { cffi_wrapper!(|in_program: WFToken, in_scope: u32, in_index: usize| {
let mut pool = pull_writer!(PROGRAM_POOL)?; let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?; let program = pool.get_mut(in_program)?;
let scope = resolve_enum!(Scope, in_scope)?; let scope = resolve_enum!(WFScope, in_scope)?;
program.unlink_ext(scope.into(), in_index)?; program.unlink_ext(scope.into(), in_index)?;
Ok(()) Ok(())
}) })
@@ -649,15 +637,15 @@ pub extern "C" fn WFProgramUnlinkExt(
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFProgramQueryExt( pub extern "C" fn WFProgramQueryExt(
in_program: in_param_ty!(Token), in_program: in_param_ty!(WFToken),
in_view: in_param_ty!(u32), in_view: in_param_ty!(u32),
in_index: in_param_ty!(usize), in_index: in_param_ty!(usize),
out_ext_status: out_param_ty!(Token), out_ext_status: out_param_ty!(WFToken),
) -> bool { ) -> bool {
cffi_wrapper!(|in_program: Token, in_view: u32, in_index: usize| -> (out_ext_status: Token) { cffi_wrapper!(|in_program: WFToken, in_view: u32, in_index: usize| -> (out_ext_status: WFToken) {
let pool = pull_reader!(PROGRAM_POOL)?; let pool = pull_reader!(PROGRAM_POOL)?;
let program = pool.get(in_program)?; let program = pool.get(in_program)?;
let view = resolve_enum!(View, in_view)?; let view = resolve_enum!(WFView, in_view)?;
let ext_status = program.query_ext(view.into(), in_index)?; let ext_status = program.query_ext(view.into(), in_index)?;
let token = match ext_status { let token = match ext_status {
@@ -676,8 +664,8 @@ pub extern "C" fn WFProgramQueryExt(
// region: Extension Status // region: Extension Status
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFExtStatusDestroy(in_ext_status: in_param_ty!(Token)) -> bool { pub extern "C" fn WFExtStatusDestroy(in_ext_status: in_param_ty!(WFToken)) -> bool {
cffi_wrapper!(|in_ext_status: Token| { cffi_wrapper!(|in_ext_status: WFToken| {
let mut pool = pull_writer!(EXT_STATUS_POOL)?; let mut pool = pull_writer!(EXT_STATUS_POOL)?;
Ok(pool.free(in_ext_status)?) Ok(pool.free(in_ext_status)?)
}) })
@@ -685,10 +673,10 @@ pub extern "C" fn WFExtStatusDestroy(in_ext_status: in_param_ty!(Token)) -> bool
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFExtStatusGetName( pub extern "C" fn WFExtStatusGetName(
in_ext_status: in_param_ty!(Token), in_ext_status: in_param_ty!(WFToken),
out_name: out_param_ty!(CStyleString), out_name: out_param_ty!(WFCString),
) -> bool { ) -> bool {
cffi_wrapper!(|in_ext_status: Token| -> (out_name: CStyleString) { cffi_wrapper!(|in_ext_status: WFToken| -> (out_name: WFCString) {
let pool = pull_reader!(EXT_STATUS_POOL)?; let pool = pull_reader!(EXT_STATUS_POOL)?;
let ext_status = pool.get(in_ext_status)?; let ext_status = pool.get(in_ext_status)?;
@@ -699,18 +687,83 @@ pub extern "C" fn WFExtStatusGetName(
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFExtStatusGetIcon( pub extern "C" fn WFExtStatusGetIcon(
in_ext_status: in_param_ty!(Token), in_ext_status: in_param_ty!(WFToken),
out_icon: out_param_ty!(HICON), out_icon: out_param_ty!(WFHICON),
) -> bool { ) -> bool {
cffi_wrapper!(|in_ext_status: Token| -> (out_icon: HICON) { cffi_wrapper!(|in_ext_status: WFToken| -> (out_icon: WFHICON) {
let pool = pull_reader!(EXT_STATUS_POOL)?; let pool = pull_reader!(EXT_STATUS_POOL)?;
let ext_status = pool.get(in_ext_status)?; let ext_status = pool.get(in_ext_status)?;
let icon = match ext_status.get_icon() { let icon = ext_status.get_icon();
Some(icon) => icon.get_icon(), Ok(icon.get_icon())
None => ffi_types::INVALID_HICON, })
}; }
Ok(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())
}) })
} }
@@ -719,8 +772,8 @@ pub extern "C" fn WFExtStatusGetIcon(
// region: Icon Resource // region: Icon Resource
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFIconRcDestroy(in_icon_rc: in_param_ty!(Token)) -> bool { pub extern "C" fn WFIconRcDestroy(in_icon_rc: in_param_ty!(WFToken)) -> bool {
cffi_wrapper!(|in_icon_rc: Token| { cffi_wrapper!(|in_icon_rc: WFToken| {
let mut pool = pull_writer!(ICON_RC_POOL)?; let mut pool = pull_writer!(ICON_RC_POOL)?;
Ok(pool.free(in_icon_rc)?) Ok(pool.free(in_icon_rc)?)
}) })
@@ -728,10 +781,10 @@ pub extern "C" fn WFIconRcDestroy(in_icon_rc: in_param_ty!(Token)) -> bool {
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFIconRcGetIcon( pub extern "C" fn WFIconRcGetIcon(
in_icon_rc: in_param_ty!(Token), in_icon_rc: in_param_ty!(WFToken),
out_icon: out_param_ty!(HICON), out_icon: out_param_ty!(WFHICON),
) -> bool { ) -> bool {
cffi_wrapper!(|in_icon_rc: Token| -> (out_icon: HICON) { cffi_wrapper!(|in_icon_rc: WFToken| -> (out_icon: WFHICON) {
let pool = pull_reader!(ICON_RC_POOL)?; let pool = pull_reader!(ICON_RC_POOL)?;
let icon_rc = pool.get(in_icon_rc)?; let icon_rc = pool.get(in_icon_rc)?;
@@ -740,41 +793,3 @@ pub extern "C" fn WFIconRcGetIcon(
} }
// endregion // endregion
// region: File Extension
#[unsafe(no_mangle)]
pub extern "C" fn WFExtDestroy(in_ext: in_param_ty!(Token)) -> bool {
cffi_wrapper!(|in_ext: Token| {
let mut pool = pull_writer!(EXT_POOL)?;
Ok(pool.free(in_ext)?)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFExtGetInner(
in_ext: in_param_ty!(Token),
out_inner: out_param_ty!(CStyleString),
) -> bool {
cffi_wrapper!(|in_ext: Token| -> (out_inner: CStyleString) {
let pool = pull_reader!(EXT_POOL)?;
let ext = pool.get(in_ext)?;
cstr_ffi::set_ffi_string(ext.inner())?;
Ok(cstr_ffi::get_ffi_string())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFExtGetDottedInner(
in_ext: in_param_ty!(Token),
out_inner: out_param_ty!(CStyleString),
) -> bool {
cffi_wrapper!(|in_ext: Token| -> (out_inner: CStyleString) {
let pool = pull_reader!(EXT_POOL)?;
let ext = pool.get(in_ext)?;
cstr_ffi::set_ffi_string(&ext.dotted_inner())?;
Ok(cstr_ffi::get_ffi_string())
})
}
// endregion
+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. For the syntax of this executable's parameters, execute it with `--help` for more.
+1 -1
View File
@@ -63,7 +63,7 @@ impl Manifest {
#[derive(Debug, TeError)] #[derive(Debug, TeError)]
pub enum ParseSchemaError { pub enum ParseSchemaError {
/// Error when operating with schema. /// Error when operating with schema.
#[error("{0}")] #[error("failed on parsing manifest file to Schema: {0}")]
Schema(#[from] wfassoc::highlevel::SchemaError) Schema(#[from] wfassoc::highlevel::SchemaError)
} }
+18 -14
View File
@@ -12,20 +12,21 @@ use toml;
#[derive(Debug, TeError)] #[derive(Debug, TeError)]
pub enum Error { pub enum Error {
/// Error when parsing Manifest TOML file. /// Error when parsing Manifest TOML file.
#[error("{0}")] #[error("initialization error: {0}")]
ParseManifest(#[from] manifest::ParseManifestError), ParseManifest(#[from] manifest::ParseManifestError),
/// Error when parsing Manifest into Schema. /// Error when parsing Manifest into Schema.
#[error("{0}")] #[error("initialization error: {0}")]
ParseSchema(#[from] manifest::ParseSchemaError), ParseSchema(#[from] manifest::ParseSchemaError),
/// Error when serializing TOML
#[error("initialization error: {0}")]
SerializeToml(#[from] toml::ser::Error),
/// Error when parsing Schema into Program. /// Error when parsing Schema into Program.
#[error("{0}")] #[error("wfassoc error: {0}")]
ParseProgram(#[from] wfassoc::highlevel::ParseProgramError), ParseProgram(#[from] wfassoc::highlevel::ParseProgramError),
/// Error when operating Program. /// Error when operating Program.
#[error("{0}")] #[error("wfassoc error: {0}")]
Program(#[from] wfassoc::highlevel::ProgramError), Program(#[from] wfassoc::highlevel::ProgramError),
/// Error when serializing TOML
#[error("{0}")]
SerializeToml(#[from] toml::ser::Error),
/// Find duplicated name when converting extension name to index /// Find duplicated name when converting extension name to index
#[error("given extension name {0} has been specified more than one time")] #[error("given extension name {0} has been specified more than one time")]
@@ -34,8 +35,8 @@ pub enum Error {
#[error("given extension name {0} is not presented in application")] #[error("given extension name {0} is not presented in application")]
BadExtName(String), BadExtName(String),
/// Find star (*) extension name with other extension names when converting extension name to index /// Find star (*) extension name with other extension names when converting extension name to index
#[error("wildcard extension name \"*\" is not allowed to be used with other extension names")] #[error(r#"wildcard extension name "*" is not allowed to be used with other extension names"#)]
ExclusiveStarExtName(String), ExclusiveStarExtName,
} }
/// Result type used in this module. /// Result type used in this module.
@@ -60,7 +61,7 @@ fn stringified_exts_to_indices(
// Check for star (*) with other extensions // Check for star (*) with other extensions
let has_star = exts.iter().any(|ext| ext == "*"); let has_star = exts.iter().any(|ext| ext == "*");
if has_star && exts.len() > 1 { if has_star && exts.len() > 1 {
return Err(Error::ExclusiveStarExtName("*".to_string())); return Err(Error::ExclusiveStarExtName);
} }
// If star is present alone, return fixed list from zero to the maximum ext index. // If star is present alone, return fixed list from zero to the maximum ext index.
@@ -124,9 +125,9 @@ fn run_ext_unlink(
) -> Result<()> { ) -> Result<()> {
let exts = stringified_exts_to_indices(&program, exts)?; let exts = stringified_exts_to_indices(&program, exts)?;
for index in exts { for index in exts {
program.link_ext(scope, index)?; program.unlink_ext(scope, index)?;
} }
println!("File extension now is unlinked."); println!("File extension now is unlinked.");
Ok(()) Ok(())
} }
@@ -139,9 +140,12 @@ fn run_ext_list(
// Fetch info // Fetch info
let mut ext_list: HashMap<String, Option<String>> = HashMap::new(); let mut ext_list: HashMap<String, Option<String>> = HashMap::new();
for index in 0..program.exts_len() { for index in 0..program.exts_len() {
let ext = program.get_ext(index)?; let self_ext_status = program.resolve_ext(index)?;
let status = program.query_ext(view, index)?; let status = program.query_ext(view, index)?;
ext_list.insert(ext.dotted_inner(), status.map(|s| s.get_name().to_string())); ext_list.insert(
self_ext_status.get_dotted_ext(),
status.map(|s| s.get_name().to_string()),
);
} }
// Output by styles // Output by styles
+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. 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. 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, If you are a programmer who want to take a deep into the internal implementations,
see `win32` module and its submodules for detail. 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.
+3 -1
View File
@@ -1,3 +1,5 @@
//! The module gives a convenient highlevel wrapper for lowlevel file association operations.
use crate::lowlevel; use crate::lowlevel;
// region: Utilities // region: Utilities
@@ -27,7 +29,7 @@ mod schema;
mod program; mod program;
pub use schema::{Schema, SchemaError}; pub use schema::{Schema, SchemaError};
pub use program::{Program, ParseProgramError, ProgramError, ProgramExtStatus}; pub use program::{Program, ParseProgramError, ProgramError, ProgramSelfExtStatus, ProgramExtStatus};
pub use lowlevel::{Scope, View}; pub use lowlevel::{Scope, View};
// endregion // endregion
+187 -51
View File
@@ -17,35 +17,38 @@ use thiserror::Error as TeError;
/// Error occurs when trying converting [Schema] into [Program]. /// Error occurs when trying converting [Schema] into [Program].
#[derive(Debug, TeError)] #[derive(Debug, TeError)]
pub enum ParseProgramError { pub enum ParseProgramError {
#[error("{0}")] #[error("parsing into Program error: {0}")]
BadExtBody(#[from] concept::BadExtBodyError), BadExtBody(#[from] concept::BadExtBodyError),
#[error("{0}")] #[error("parsing into Program error: {0}")]
BadProgIdPart(#[from] concept::BadProgIdPartError), BadProgIdPart(#[from] concept::BadProgIdPartError),
#[error("{0}")] #[error("parsing into Program error: {0}")]
BadFileName(#[from] concept::BadFileNameError), BadFileName(#[from] concept::BadFileNameError),
#[error("{0}")] #[error("parsing into Program error: {0}")]
ParseClsid(#[from] concept::ParseClsidError),
#[error("parsing into Program error: {0}")]
ParseCmdLine(#[from] concept::ParseCmdLineError), ParseCmdLine(#[from] concept::ParseCmdLineError),
#[error("{0}")] #[error("parsing into Program error: {0}")]
CastOsStr(#[from] utilities::CastOsStrError), CastOsStr(#[from] utilities::CastOsStrError),
#[error("given path doesn't has legal file name part")]
#[error("given path doesn't has legal file name part when parsing into Program")]
NoFileNamePart, NoFileNamePart,
#[error("given path doesn't has legal directory part")] #[error("given path doesn't has legal directory part when parsing into Program")]
NoDirNamePart, NoDirNamePart,
#[error("given identifier is not presented in dict")] #[error("identifier {0} is unexpected not presented in HashMap when parsing into Program")]
NoSuchIdentifier, NoSuchIdentifier(String),
#[error("extension name should not be empty")] #[error("bad program identifier found when parsing into Program")]
EmptyExtension,
#[error("given program identifier is not allowed")]
BadIdentifier, BadIdentifier,
} }
/// Error occurs when operating with [Program]. /// Error occurs when operating with [Program].
#[derive(Debug, TeError)] #[derive(Debug, TeError)]
pub enum ProgramError { pub enum ProgramError {
#[error("{0}")] #[error("Program operation error: {0}")]
Lowlevel(#[from] lowlevel::Error), Lowlevel(#[from] lowlevel::Error),
#[error("given index is invalid")] #[error("Program operation error: {0}")]
BadIndex, LoadIconRc(#[from] concept::LoadIconRcError),
#[error("given file extension index {0} is invalid when visiting Program")]
BadIndex(usize),
} }
// endregion // endregion
@@ -53,11 +56,18 @@ pub enum ProgramError {
// region: Program // region: Program
/// Program is a complete and immutable program representer /// Program is a complete and immutable program representer
#[derive(Debug)]
pub struct Program { pub struct Program {
app_paths_key: lowlevel::AppPathsKey, app_paths_key: lowlevel::AppPathsKey,
applications_key: lowlevel::ApplicationsKey, applications_key: lowlevel::ApplicationsKey,
app_path: String, app_path: String,
app_file_name: String,
app_dir_path: 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>>, name: Option<Arc<ProgramStr>>,
icon: Option<Arc<ProgramIcon>>, icon: Option<Arc<ProgramIcon>>,
behavior: Option<Arc<ProgramBehavior>>, behavior: Option<Arc<ProgramBehavior>>,
@@ -99,6 +109,8 @@ impl Program {
.ok_or(ParseProgramError::NoDirNamePart) .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>( fn flat_hashmap<V, U, F>(
hashmap: &HashMap<String, V>, hashmap: &HashMap<String, V>,
f: F, f: F,
@@ -115,6 +127,8 @@ impl Program {
Ok((vector, indexmap)) 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>( fn resolve_index<T>(
key: &str, key: &str,
vector: &Vec<Arc<T>>, vector: &Vec<Arc<T>>,
@@ -125,7 +139,7 @@ impl Program {
.get(*index) .get(*index)
.expect("unexpected invalid index") .expect("unexpected invalid index")
.clone()), .clone()),
None => Err(ParseProgramError::NoSuchIdentifier), None => Err(ParseProgramError::NoSuchIdentifier(key.to_string())),
} }
} }
@@ -172,6 +186,9 @@ impl Program {
let app_paths_key = lowlevel::AppPathsKey::new(key.clone()); let app_paths_key = lowlevel::AppPathsKey::new(key.clone());
let applications_key = lowlevel::ApplicationsKey::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, // Build string, icon and behavior list,
// and build mapper at the same time. // and build mapper at the same time.
let (strs, strs_index_map) = Self::flat_hashmap(schema.get_strs(), |entry| { let (strs, strs_index_map) = Self::flat_hashmap(schema.get_strs(), |entry| {
@@ -254,7 +271,9 @@ impl Program {
app_paths_key, app_paths_key,
applications_key, applications_key,
app_path, app_path,
app_file_name,
app_dir_path, app_dir_path,
clsid,
name, name,
icon, icon,
behavior, behavior,
@@ -268,39 +287,100 @@ impl Program {
} }
impl Program { impl Program {
pub fn resolve_name(&self) -> Result<Option<String>, ProgramError> { /// Resolve the display name of this application.
Ok(self ///
/// 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 .name
.as_ref() .as_ref()
.map(|name| name.inner.extract().ok()) .map(|name| name.inner.extract().ok())
.flatten()) .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())
} }
pub fn resolve_icon(&self) -> Result<Option<concept::IconRc>, ProgramError> { /// Resolve the icon of this application.
Ok(self ///
/// 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 .icon
.as_ref() .as_ref()
.map(|icon| icon.inner.extract(concept::IconSizeKind::Small).ok()) .map(|icon| icon.inner.extract(concept::IconSizeKind::Small).ok())
.flatten()) .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 { pub fn exts_len(&self) -> usize {
self.ext_keys.len() self.ext_keys.len()
} }
pub fn get_ext(&self, index: usize) -> Result<&concept::Ext, ProgramError> { /// Find the index of a file extension by its body (without leading dot).
match self.ext_keys.get(index) { ///
Some(program_key) => { /// Returns `None` if the given extension is not associated with this program.
let ext_key = &program_key.ext_key;
Ok(ext_key.inner())
}
None => Err(ProgramError::BadIndex),
}
}
pub fn find_ext(&self, body: &str) -> Option<usize> { pub fn find_ext(&self, body: &str) -> Option<usize> {
self.ext_keys_map.get(body).copied() 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 { impl Program {
@@ -341,7 +421,7 @@ impl Program {
for program_key in &mut self.ext_keys { for program_key in &mut self.ext_keys {
let progid_key = &mut program_key.progid_key; let progid_key = &mut program_key.progid_key;
debug_println!( debug_println!(
"Adding ProgId \"{0}\" subkey...", r#"Adding ProgId "{0}" subkey..."#,
progid_key.inner().to_string() progid_key.inner().to_string()
); );
@@ -356,6 +436,7 @@ impl Program {
// Add this progid to file extension "open with" list. // Add this progid to file extension "open with" list.
let ext_key = &mut program_key.ext_key; let ext_key = &mut program_key.ext_key;
ext_key.ensure(scope)?;
ext_key.add_into_open_with_progids(scope, progid_key.inner())?; ext_key.add_into_open_with_progids(scope, progid_key.inner())?;
} }
@@ -383,7 +464,7 @@ impl Program {
for program_key in &mut self.ext_keys { for program_key in &mut self.ext_keys {
let progid_key = &mut program_key.progid_key; let progid_key = &mut program_key.progid_key;
debug_println!( debug_println!(
"Deleting ProgId \"{0}\" subkey...", r#"Deleting ProgId "{0}" subkey..."#,
progid_key.inner().to_string() progid_key.inner().to_string()
); );
@@ -392,9 +473,12 @@ impl Program {
// there is no need to reset the default open way of file extension. // there is no need to reset the default open way of file extension.
// So we simply remove it from "open with" list. // So we simply remove it from "open with" list.
// Remove this ProgId from file extension "open with" list. // Remove this ProgId from file extension "open with" list,
// if this file extension is existing
let ext_key = &mut program_key.ext_key; let ext_key = &mut program_key.ext_key;
ext_key.remove_from_open_with_progids(scope, progid_key.inner())?; if ext_key.is_exist(scope.into())? {
ext_key.remove_from_open_with_progids(scope, progid_key.inner())?;
}
// Delete ProgId subkey // Delete ProgId subkey
progid_key.delete(scope)?; progid_key.delete(scope)?;
@@ -409,8 +493,7 @@ impl Program {
/// Check whether this application has been registered in given view. /// Check whether this application has been registered in given view.
/// ///
/// Please note that this is a rough check and do not validate any data. /// Please note that this is a rough check and do not validate any data.
/// /// The return value only ensures the pre-requirements of [Self::register] and [Self::unregister].
/// The return value only ensures the pre-requirement of `register` and `unregister`.
pub fn is_registered(&self, scope: Scope) -> Result<bool, ProgramError> { pub fn is_registered(&self, scope: Scope) -> Result<bool, ProgramError> {
// Check App Paths subkey. // Check App Paths subkey.
debug_println!("Checking App Paths subkey..."); debug_println!("Checking App Paths subkey...");
@@ -429,7 +512,7 @@ impl Program {
for program_key in &self.ext_keys { for program_key in &self.ext_keys {
let progid_key = &program_key.progid_key; let progid_key = &program_key.progid_key;
debug_println!( debug_println!(
"Checking ProgId \"{0}\" subkey...", r#"Checking ProgId "{0}" subkey..."#,
progid_key.inner().to_string() progid_key.inner().to_string()
); );
@@ -442,13 +525,14 @@ impl Program {
Ok(true) 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> { pub fn link_ext(&mut self, scope: Scope, index: usize) -> Result<(), ProgramError> {
match self.ext_keys.get_mut(index) { match self.ext_keys.get_mut(index) {
Some(program_key) => { Some(program_key) => {
let ext_key = &mut program_key.ext_key; let ext_key = &mut program_key.ext_key;
let progid_key = &program_key.progid_key; let progid_key = &program_key.progid_key;
debug_println!( debug_println!(
"Linking ProgId \"{0}\" to extension \"{1}\" subkey...", r#"Linking ProgId "{0}" to extension "{1}" subkey..."#,
progid_key.inner().to_string(), progid_key.inner().to_string(),
ext_key.inner().to_string() ext_key.inner().to_string()
); );
@@ -457,7 +541,7 @@ impl Program {
ext_key.ensure(scope)?; ext_key.ensure(scope)?;
ext_key.set_default(scope, Some(progid_key.inner()))?; ext_key.set_default(scope, Some(progid_key.inner()))?;
} }
None => return Err(ProgramError::BadIndex), None => return Err(ProgramError::BadIndex(index)),
}; };
// Everything is okey. // Everything is okey.
@@ -466,12 +550,13 @@ impl Program {
Ok(()) 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> { pub fn unlink_ext(&mut self, scope: Scope, index: usize) -> Result<(), ProgramError> {
match self.ext_keys.get_mut(index) { match self.ext_keys.get_mut(index) {
Some(program_key) => { Some(program_key) => {
let ext_key = &mut program_key.ext_key; let ext_key = &mut program_key.ext_key;
debug_println!( debug_println!(
"Unlinking for extension \"{0}\" subkey...", r#"Unlinking for extension "{0}" subkey..."#,
ext_key.inner().to_string() ext_key.inner().to_string()
); );
@@ -479,7 +564,7 @@ impl Program {
ext_key.ensure(scope)?; ext_key.ensure(scope)?;
ext_key.set_default(scope, None)?; ext_key.set_default(scope, None)?;
} }
None => return Err(ProgramError::BadIndex), None => return Err(ProgramError::BadIndex(index)),
} }
// Everything is okey. // Everything is okey.
@@ -488,6 +573,10 @@ impl Program {
Ok(()) 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( pub fn query_ext(
&self, &self,
view: View, view: View,
@@ -497,7 +586,7 @@ impl Program {
Some(program_key) => { Some(program_key) => {
let ext_key = &program_key.ext_key; let ext_key = &program_key.ext_key;
debug_println!( debug_println!(
"Querying for extension \"{0}\"subkey...", r#"Querying for extension "{0}"subkey..."#,
ext_key.inner().to_string() ext_key.inner().to_string()
); );
@@ -535,16 +624,19 @@ impl Program {
.flatten(); .flatten();
} }
let name = name.unwrap_or(progid_key.inner().to_string()); let name = name.unwrap_or(progid_key.inner().to_string());
// Now try to fetch icon. // Now try to fetch icon and fallback to system default file icon.
let icon = progid_key let icon = progid_key
.get_default_icon(view)? .get_default_icon(view)?
.map(|ico| ico.extract(concept::IconSizeKind::Small).ok()) .map(|ico| ico.extract(concept::IconSizeKind::Small).ok())
.flatten(); .flatten()
.unwrap_or(concept::IconRc::GENERIC_DOCUMENT(
concept::IconSizeKind::Small,
)?);
// Okey, return it. // Okey, return it.
Ok(Some(ProgramExtStatus::new(name, icon))) Ok(Some(ProgramExtStatus::new(name, icon)))
} }
None => Err(ProgramError::BadIndex), None => Err(ProgramError::BadIndex(index)),
} }
} }
} }
@@ -589,16 +681,60 @@ struct ProgramProgIdExtKey {
// region: Exposed Stuff // 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. /// Exposed struct representing the default associated program of specific file extension.
/// ///
/// The data including the diaplay name and icon. /// The data including the diaplay name and icon.
#[derive(Debug)]
pub struct ProgramExtStatus { pub struct ProgramExtStatus {
name: String, name: String,
icon: Option<concept::IconRc>, icon: concept::IconRc,
} }
impl ProgramExtStatus { impl ProgramExtStatus {
fn new(name: String, icon: Option<concept::IconRc>) -> Self { /// Create a new `ProgramExtStatus`.
fn new(name: String, icon: concept::IconRc) -> Self {
Self { name, icon } Self { name, icon }
} }
@@ -613,8 +749,8 @@ impl ProgramExtStatus {
/// Get the icon of this program. /// Get the icon of this program.
/// ///
/// Due to the icon is optional, if there is no icon, return None. /// Due to the icon is optional, if there is no icon, return None.
pub fn get_icon(&self) -> Option<&concept::IconRc> { pub fn get_icon(&self) -> &concept::IconRc {
self.icon.as_ref() &self.icon
} }
} }
+46 -2
View File
@@ -7,7 +7,7 @@ use super::{Program, ParseProgramError};
/// Error occurs when operating with [Schema]. /// Error occurs when operating with [Schema].
#[derive(Debug, TeError)] #[derive(Debug, TeError)]
pub enum SchemaError { pub enum SchemaError {
#[error("duplicate key: {0}")] #[error("duplicate key in Schema operation: {0}")]
DuplicateKey(String), DuplicateKey(String),
} }
@@ -38,6 +38,7 @@ pub struct Schema {
} }
impl Schema { impl Schema {
/// Create an empty `Schema` with all fields set to their defaults.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
identifier: String::new(), identifier: String::new(),
@@ -76,22 +77,40 @@ impl Schema {
self.path = exe_path.to_string(); 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) -> () { pub fn set_clsid(&mut self, clsid: &str) -> () {
self.clsid = clsid.to_string(); 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>) -> () { pub fn set_name(&mut self, name: Option<&str>) -> () {
self.name = name.map(|n| n.to_string()); 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>) -> () { pub fn set_icon(&mut self, icon: Option<&str>) -> () {
self.icon = icon.map(|i| i.to_string()); 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>) -> () { pub fn set_behavior(&mut self, behavior: Option<&str>) -> () {
self.behavior = behavior.map(|b| b.to_string()); 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> { pub fn add_str(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
match self.strs.insert(name.to_string(), value.to_string()) { match self.strs.insert(name.to_string(), value.to_string()) {
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())), Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
@@ -99,6 +118,9 @@ impl Schema {
} }
} }
/// 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> { pub fn add_icon(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
match self.icons.insert(name.to_string(), value.to_string()) { match self.icons.insert(name.to_string(), value.to_string()) {
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())), Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
@@ -106,6 +128,9 @@ impl Schema {
} }
} }
/// 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> { pub fn add_behavior(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
match self.behaviors.insert(name.to_string(), value.to_string()) { match self.behaviors.insert(name.to_string(), value.to_string()) {
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())), Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
@@ -116,6 +141,8 @@ impl Schema {
/// Add a file extension to the schema. /// Add a file extension to the schema.
/// ///
/// The parameter `ext` is the file extension without leading dot `.`. /// 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( pub fn add_ext(
&mut self, &mut self,
ext: &str, ext: &str,
@@ -135,42 +162,55 @@ impl Schema {
} }
impl Schema { impl Schema {
/// Get the identifier of the schema.
pub(super) fn get_identifier(&self) -> &str { pub(super) fn get_identifier(&self) -> &str {
&self.identifier &self.identifier
} }
/// Get the absolute path to the executable file.
pub(super) fn get_path(&self) -> &str { pub(super) fn get_path(&self) -> &str {
&self.path &self.path
} }
/// Get the CLSID (Class Identifier) of the application.
pub(super) fn get_clsid(&self) -> &str { pub(super) fn get_clsid(&self) -> &str {
&self.clsid &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> { pub(super) fn get_name(&self) -> Option<&str> {
self.name.as_ref().map(|v| v.as_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> { pub(super) fn get_icon(&self) -> Option<&str> {
self.icon.as_ref().map(|v| v.as_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> { pub(super) fn get_behavior(&self) -> Option<&str> {
self.icon.as_ref().map(|v| v.as_str()) self.behavior.as_ref().map(|v| v.as_str())
} }
/// Get a reference to the string resources map.
pub(super) fn get_strs(&self) -> &HashMap<String, String> { pub(super) fn get_strs(&self) -> &HashMap<String, String> {
&self.strs &self.strs
} }
/// Get a reference to the icon resources map.
pub(super) fn get_icons(&self) -> &HashMap<String, String> { pub(super) fn get_icons(&self) -> &HashMap<String, String> {
&self.icons &self.icons
} }
/// Get a reference to the behavior resources map.
pub(super) fn get_behaviors(&self) -> &HashMap<String, String> { pub(super) fn get_behaviors(&self) -> &HashMap<String, String> {
&self.behaviors &self.behaviors
} }
/// Get a reference to the file extensions map.
pub(super) fn get_exts(&self) -> &HashMap<String, SchemaExt> { pub(super) fn get_exts(&self) -> &HashMap<String, SchemaExt> {
&self.exts &self.exts
} }
@@ -189,6 +229,7 @@ pub(crate) struct SchemaExt {
} }
impl SchemaExt { impl SchemaExt {
/// Create a new `SchemaExt` with the given name, icon key and behavior key.
fn new(name: &str, icon: &str, behavior: &str) -> Self { fn new(name: &str, icon: &str, behavior: &str) -> Self {
Self { Self {
name: name.to_string(), name: name.to_string(),
@@ -199,14 +240,17 @@ impl SchemaExt {
} }
impl SchemaExt { impl SchemaExt {
/// Get the key referencing the named string resource for display name.
pub(super) fn get_name(&self) -> &str { pub(super) fn get_name(&self) -> &str {
&self.name &self.name
} }
/// Get the key referencing the named icon resource for file icon.
pub(super) fn get_icon(&self) -> &str { pub(super) fn get_icon(&self) -> &str {
&self.icon &self.icon
} }
/// Get the key referencing the named behavior resource.
pub(super) fn get_behavior(&self) -> &str { pub(super) fn get_behavior(&self) -> &str {
&self.behavior &self.behavior
} }
+1 -1
View File
@@ -1,5 +1,5 @@
//! This crate provide utilities fetching and manilupating Windows file association. //! This crate provide utilities fetching and manilupating Windows file association.
//! All code under crate are following Microsoft document: https://learn.microsoft.com/en-us/windows/win32/shell/customizing-file-types-bumper //! All code under crate are following Microsoft document: <https://learn.microsoft.com/en-us/windows/win32/shell/customizing-file-types-bumper>
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
compile_error!("Crate wfassoc is only supported on Windows."); compile_error!("Crate wfassoc is only supported on Windows.");
+66 -18
View File
@@ -1,3 +1,5 @@
//! The module containing lowlevel operation involving file association.
use crate::win32::{concept, regext, utilities}; use crate::win32::{concept, regext, utilities};
use std::fmt::Display; use std::fmt::Display;
use std::str::FromStr; use std::str::FromStr;
@@ -10,27 +12,27 @@ use winreg::enums::{KEY_READ, KEY_WRITE};
/// Error occurs in this module. /// Error occurs in this module.
#[derive(Debug, TeError)] #[derive(Debug, TeError)]
pub enum Error { pub enum Error {
#[error("can not perform this operation because lack essential privilege")] #[error("some lowlevel operations require escalating privilege")]
NoPrivilege, NoPrivilege,
#[error("given registry key is inexistant")] #[error("given lowlevel registry key is not presented")]
InexistantKey, InexistantKey,
#[error("registry operation error: {0}")] #[error("lowlevel registry operation error: {0}")]
BadRegOp(#[from] std::io::Error), BadRegOp(#[from] std::io::Error),
#[error("{0}")] #[error("unexpected blank registry key in lowlevel registry operation")]
UnexpectedBlankKey(#[from] regext::BlankPathError), UnexpectedBlankKey(#[from] regext::BlankPathError),
#[error("{0}")] #[error("lowlevel operation error: {0}")]
ExpandEnvVar(#[from] concept::ExpandEnvVarError), ExpandEnvVar(#[from] concept::ExpandEnvVarError),
#[error("{0}")] #[error("lowlevel operation error: {0}")]
LoadIconRc(#[from] concept::LoadIconRcError), LoadIconRc(#[from] concept::LoadIconRcError),
#[error("{0}")] #[error("lowlevel operation error: {0}")]
LoadStrRc(#[from] concept::LoadStrRcError), LoadStrRc(#[from] concept::LoadStrRcError),
#[error("{0}")] #[error("lowlevel operation error: {0}")]
ParseVerb(#[from] concept::ParseVerbError), ParseVerb(#[from] concept::ParseVerbError),
#[error("{0}")] #[error("lowlevel operation error: {0}")]
ParseCmdLine(#[from] concept::ParseCmdLineError), ParseCmdLine(#[from] concept::ParseCmdLineError),
#[error("{0}")] #[error("lowlevel operation error: {0}")]
ParseExt(#[from] concept::ParseExtError), ParseExt(#[from] concept::ParseExtError),
} }
@@ -58,6 +60,7 @@ pub enum Scope {
pub struct TryFromViewError {} pub struct TryFromViewError {}
impl TryFromViewError { impl TryFromViewError {
/// Creates a new `TryFromViewError`.
fn new() -> Self { fn new() -> Self {
Self {} Self {}
} }
@@ -106,9 +109,13 @@ impl From<Scope> for View {
/// The enum representing a losse Programmatic Identifiers (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. /// In real world, not all software developers are willing
/// They use string which do not have any regulation as ProgId. /// 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. /// 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)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LosseProgId { pub enum LosseProgId {
Plain(String), Plain(String),
@@ -151,6 +158,11 @@ impl From<concept::ProgId> for LosseProgId {
/// In real usage, programmer can use Icon Reference String, /// In real usage, programmer can use Icon Reference String,
/// or a plain string pointing to a icon file as the icon setting value. /// or a plain string pointing to a icon file as the icon setting value.
/// This enum is designed for handling this scenario. /// 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)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum IconResVariant { pub enum IconResVariant {
Plain(String), Plain(String),
@@ -158,7 +170,12 @@ pub enum IconResVariant {
} }
impl IconResVariant { impl IconResVariant {
/// Extracts the icon resource with the given size kind.
pub fn extract(&self, kind: concept::IconSizeKind) -> Result<concept::IconRc> { 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 { let rc = match self {
IconResVariant::Plain(v) => concept::IconRc::with_ico_file(v.as_str(), kind)?, IconResVariant::Plain(v) => concept::IconRc::with_ico_file(v.as_str(), kind)?,
IconResVariant::RefStr(v) => { IconResVariant::RefStr(v) => {
@@ -214,6 +231,9 @@ impl From<concept::IconRefStr> for IconResVariant {
/// In real usage, programmer can use String Reference String, /// In real usage, programmer can use String Reference String,
/// or a plain string as the string setting value. /// or a plain string as the string setting value.
/// This enum is designed for handling this scenario. /// 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)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum StrResVariant { pub enum StrResVariant {
Plain(String), Plain(String),
@@ -221,7 +241,12 @@ pub enum StrResVariant {
} }
impl StrResVariant { impl StrResVariant {
/// Resolves this string resource variant into a plain string.
pub fn extract(&self) -> Result<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 { let rv = match self {
// For plain string, we just simply clone it. // For plain string, we just simply clone it.
StrResVariant::Plain(v) => v.clone(), StrResVariant::Plain(v) => v.clone(),
@@ -276,6 +301,12 @@ impl From<concept::StrRefStr> for StrResVariant {
// region: Shell Verb // region: Shell Verb
/// The struct representing a shell verb pair. /// 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)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ShellVerb { pub struct ShellVerb {
verb: concept::Verb, verb: concept::Verb,
@@ -283,14 +314,17 @@ pub struct ShellVerb {
} }
impl ShellVerb { impl ShellVerb {
/// Creates a new `ShellVerb` from a verb and a command.
pub fn new(verb: concept::Verb, command: concept::CmdLine) -> Self { pub fn new(verb: concept::Verb, command: concept::CmdLine) -> Self {
Self { verb, command } Self { verb, command }
} }
/// Returns the verb of this shell verb pair.
pub fn get_verb(&self) -> &concept::Verb { pub fn get_verb(&self) -> &concept::Verb {
&self.verb &self.verb
} }
/// Returns the command of this shell verb pair.
pub fn get_command(&self) -> &concept::CmdLine { pub fn get_command(&self) -> &concept::CmdLine {
&self.command &self.command
} }
@@ -304,7 +338,7 @@ impl ShellVerb {
// region: Opened Key // region: Opened Key
/// Internal used struct representing the result of opening scope or view. /// Internal used struct representing the result about opening registry key in given scope or view.
#[derive(Debug)] #[derive(Debug)]
struct OpenedKey { struct OpenedKey {
/// The parent key of opened key which must be presented. /// The parent key of opened key which must be presented.
@@ -314,6 +348,7 @@ struct OpenedKey {
} }
impl OpenedKey { 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 { fn new(parent_key: RegKey, this_key: Option<RegKey>) -> Self {
Self { Self {
parent_key, parent_key,
@@ -326,10 +361,10 @@ impl OpenedKey {
// region: Open Key Territory // region: Open Key Territory
/// The territory of opening key. /// Internal used enum representing the territory of opening registry key.
/// ///
/// Scope and View will finally be converted into this type, /// [Scope] and [View] will finally be converted into this type,
/// and delivered to an uniform function to open key. /// and delivered to an uniform function to open registry key.
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
enum OpenKeyTerritory { enum OpenKeyTerritory {
System, System,
@@ -360,7 +395,7 @@ impl From<View> for OpenKeyTerritory {
// region: Open Key Purpose // region: Open Key Purpose
/// The purpose of opening this key. /// The purpose of opening given registry key.
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
enum OpenKeyPurpose { enum OpenKeyPurpose {
/// Only read something. /// Only read something.
@@ -370,6 +405,7 @@ enum OpenKeyPurpose {
} }
impl OpenKeyPurpose { impl OpenKeyPurpose {
/// Converts this purpose to the corresponding registry permission flags.
fn to_permission(&self) -> u32 { fn to_permission(&self) -> u32 {
match self { match self {
OpenKeyPurpose::Read => PERM_R, OpenKeyPurpose::Read => PERM_R,
@@ -411,8 +447,17 @@ fn check_privilege(territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Resu
} }
} }
/// Remove quote pair if possible. // TODO:
// This function should be eliminated or moved to another place,
// once we finish the quote mechanism in StrRefStr and IconRefStr.
/// Internal function removing quote pair if possible.
/// ///
/// # Todo
///
/// This function actually is the compromise about the current design of this crate.
/// This function will be optimized in future.
///
/// In some cases, the path part of [concept::IconRefStr] or [concept::StrRefStr] is quoted by quote. /// In some cases, the path part of [concept::IconRefStr] or [concept::StrRefStr] is quoted by quote.
/// This can no be recognized by Win32 functions. /// This can no be recognized by Win32 functions.
/// So in this case, we should remove this quote pair. /// So in this case, we should remove this quote pair.
@@ -431,6 +476,9 @@ fn strip_quote<'a>(s: &'a str) -> &'a str {
// region: Registry Keys // region: Registry Keys
// YYC MARK:
// Load submodules and re-export them.
mod app_path_key; mod app_path_key;
mod applications_key; mod applications_key;
mod ext_key; mod ext_key;
+51 -23
View File
@@ -5,16 +5,30 @@ use crate::win32::{concept, regext};
use winreg::RegKey; use winreg::RegKey;
use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE}; 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)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AppPathsKey { pub struct AppPathsKey {
key_name: concept::FileName, key_name: concept::FileName,
} }
impl AppPathsKey { impl AppPathsKey {
/// Creates a new `AppPathsKey` with the given file name.
pub fn new(inner: concept::FileName) -> Self { pub fn new(inner: concept::FileName) -> Self {
Self { key_name: inner } Self { key_name: inner }
} }
/// Returns the inner file name of this application path key.
pub fn inner(&self) -> &concept::FileName { pub fn inner(&self) -> &concept::FileName {
&self.key_name &self.key_name
} }
@@ -23,6 +37,7 @@ impl AppPathsKey {
impl AppPathsKey { impl AppPathsKey {
const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths"; 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> { fn open_key(&self, territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<OpenedKey> {
// check privilege // check privilege
check_privilege(territory, purpose)?; check_privilege(territory, purpose)?;
@@ -49,20 +64,24 @@ impl AppPathsKey {
Ok(OpenedKey::new(app_paths, this_app)) 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> { fn open_scope_for_read(&self, scope: Scope) -> Result<OpenedKey> {
self.open_key(scope.into(), OpenKeyPurpose::Read) 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> { fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey> {
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite) 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> { pub fn is_exist(&self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_read(scope)?.this_key; let key = self.open_scope_for_read(scope)?.this_key;
Ok(key.is_some()) Ok(key.is_some())
} }
/// Ensure this application key is presented in App Paths. /// Ensure this application key is presented in App Paths key.
/// ///
/// Return true if we newly create this key, /// Return true if we newly create this key,
/// otherwise false indicating there already is an existing key. /// otherwise false indicating there already is an existing key.
@@ -79,66 +98,75 @@ impl AppPathsKey {
} }
} }
/// Delete this application key from App Paths. /// Delete this application key from App Paths key.
/// ///
/// If there is no such key in App Paths, /// Return true if we successfully delete this key,
/// this function does nothing. /// otherwise false indicating there is no such key (already deleted).
pub fn delete(&mut self, scope: Scope) -> Result<()> { pub fn delete(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?; let key = self.open_scope_for_write(scope)?;
key.parent_key Ok(regext::arbitrarily_delete_subkey_all(
.delete_subkey_all(regext::blank_path_guard(self.key_name.inner())?)?; &key.parent_key,
Ok(()) 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> { fn open_scope_for_getter(&self, scope: Scope) -> Result<RegKey> {
self.open_scope_for_read(scope)? self.open_scope_for_read(scope)?
.this_key .this_key
.ok_or(Error::InexistantKey) .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> { fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey> {
self.open_scope_for_write(scope)? self.open_scope_for_write(scope)?
.this_key .this_key
.ok_or(Error::InexistantKey) .ok_or(Error::InexistantKey)
} }
// YYC MARK:
// Reference: https://learn.microsoft.com/en-us/windows/win32/shell/app-registration#using-the-app-paths-subkey
const NAMEOF_DEFAULT: &str = ""; 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.
/// This field point to the fully qualified path to the application.
pub fn get_default(&self, scope: Scope) -> Result<String> { pub fn get_default(&self, scope: Scope) -> Result<String> {
let key = self.open_scope_for_getter(scope)?; let key = self.open_scope_for_getter(scope)?;
Ok(key.get_value(Self::NAMEOF_DEFAULT)?) Ok(key.get_value(Self::NAMEOF_DEFAULT)?)
} }
/// /// Sets the default value of this application path key.
///
/// This field should be filled with fully qualified path to the application.
pub fn set_default(&mut self, scope: Scope, value: &str) -> Result<()> { pub fn set_default(&mut self, scope: Scope, value: &str) -> Result<()> {
let key = self.open_scope_for_setter(scope)?; let key = self.open_scope_for_setter(scope)?;
key.set_value(Self::NAMEOF_DEFAULT, &value)?; key.set_value(Self::NAMEOF_DEFAULT, &value)?;
Ok(()) 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"; 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,
/// This field point to the added path for PATH environment variable. /// to enable that application can find its dependencies conveniently.
/// Usually it is the path to application directory. /// 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> { pub fn get_path(&self, scope: Scope) -> Result<String> {
let key = self.open_scope_for_getter(scope)?; let key = self.open_scope_for_getter(scope)?;
Ok(key.get_value(Self::NAMEOF_PATH)?) Ok(key.get_value(Self::NAMEOF_PATH)?)
} }
/// /// Sets the `Path` value of this application path key.
///
/// 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<()> { pub fn set_path(&mut self, scope: Scope, value: &str) -> Result<()> {
let key = self.open_scope_for_setter(scope)?; let key = self.open_scope_for_setter(scope)?;
key.set_value(Self::NAMEOF_PATH, &value)?; key.set_value(Self::NAMEOF_PATH, &value)?;
+85 -10
View File
@@ -6,16 +6,27 @@ use crate::win32::{concept, regext};
use winreg::RegKey; use winreg::RegKey;
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE}; 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)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ApplicationsKey { pub struct ApplicationsKey {
key_name: concept::FileName, key_name: concept::FileName,
} }
impl ApplicationsKey { impl ApplicationsKey {
/// Creates a new `ApplicationsKey` with the given file name.
pub fn new(inner: concept::FileName) -> Self { pub fn new(inner: concept::FileName) -> Self {
Self { key_name: inner } Self { key_name: inner }
} }
/// Returns the inner file name of this applications key.
pub fn inner(&self) -> &concept::FileName { pub fn inner(&self) -> &concept::FileName {
&self.key_name &self.key_name
} }
@@ -25,6 +36,7 @@ impl ApplicationsKey {
const FULL_APPLICATIONS: &str = "Software\\Classes\\Applications"; const FULL_APPLICATIONS: &str = "Software\\Classes\\Applications";
const PARTIAL_APPLICATIONS: &str = "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> { fn open_key(&self, territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<OpenedKey> {
// check privilege // check privilege
check_privilege(territory, purpose)?; check_privilege(territory, purpose)?;
@@ -55,19 +67,27 @@ impl ApplicationsKey {
Ok(OpenedKey::new(applications, this_app)) 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> { fn open_view_for_read(&self, view: View) -> Result<OpenedKey> {
self.open_key(view.into(), OpenKeyPurpose::Read) 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> { fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey> {
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite) 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> { pub fn is_exist(&self, view: View) -> Result<bool> {
let key = self.open_view_for_read(view)?.this_key; let key = self.open_view_for_read(view)?.this_key;
Ok(key.is_some()) 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> { pub fn ensure(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?; let key = self.open_scope_for_write(scope)?;
if let None = key.this_key { if let None = key.this_key {
@@ -81,32 +101,54 @@ impl ApplicationsKey {
} }
} }
pub fn delete(&mut self, scope: Scope) -> Result<()> { /// Delete this application key from Applications key.
///
/// Return true if we successfully delete this key,
/// otherwise false indicating there is no such key (already deleted).
pub fn delete(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?; let key = self.open_scope_for_write(scope)?;
key.parent_key Ok(regext::arbitrarily_delete_subkey_all(
.delete_subkey_all(regext::blank_path_guard(self.key_name.inner())?)?; &key.parent_key,
Ok(()) regext::blank_path_guard(self.key_name.inner())?,
)?)
} }
// YYC MARK: // YYC MARK:
// Reference: https://learn.microsoft.com/en-us/windows/win32/shell/app-registration#using-the-applications-subkey // 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> { fn open_view_for_getter(&self, view: View) -> Result<RegKey> {
self.open_view_for_read(view)? self.open_view_for_read(view)?
.this_key .this_key
.ok_or(Error::InexistantKey) .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> { fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey> {
self.open_scope_for_write(scope)? self.open_scope_for_write(scope)?
.this_key .this_key
.ok_or(Error::InexistantKey) .ok_or(Error::InexistantKey)
} }
// TODO:
// Support mutiple ShellVerb in future.
const NAMEOF_SHELL_VERB_PART1: &str = "shell"; const NAMEOF_SHELL_VERB_PART1: &str = "shell";
const NAMEOF_SHELL_VERB_PART3: &str = "command"; const NAMEOF_SHELL_VERB_PART3: &str = "command";
const NAMEOF_SHELL_VERB_PART4: &str = ""; 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>> { pub fn get_shell_verb(&self, view: View) -> Result<Option<ShellVerb>> {
let key = self.open_view_for_getter(view)?; let key = self.open_view_for_getter(view)?;
@@ -157,6 +199,15 @@ impl ApplicationsKey {
))) )))
} }
/// 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<()> { pub fn set_shell_verb(&mut self, scope: Scope, sv: Option<&ShellVerb>) -> Result<()> {
let key = self.open_scope_for_setter(scope)?; let key = self.open_scope_for_setter(scope)?;
@@ -176,7 +227,7 @@ impl ApplicationsKey {
} }
None => { None => {
// Delete shell and its all subkey. // Delete shell and its all subkey.
key.delete_subkey_all(Self::NAMEOF_SHELL_VERB_PART1)?; regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_SHELL_VERB_PART1)?;
} }
} }
@@ -186,6 +237,10 @@ impl ApplicationsKey {
const NAMEOF_DEFAULT_ICON_PART1: &str = "DefaultIcon"; const NAMEOF_DEFAULT_ICON_PART1: &str = "DefaultIcon";
const NAMEOF_DEFAULT_ICON_PART2: &str = ""; 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>> { pub fn get_default_icon(&self, view: View) -> Result<Option<IconResVariant>> {
let key = self.open_view_for_getter(view)?; let key = self.open_view_for_getter(view)?;
// Get default icon subkey // Get default icon subkey
@@ -204,6 +259,8 @@ impl ApplicationsKey {
Ok(default_icon_default_value.map(|v| IconResVariant::from(v.as_str()))) 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<()> { pub fn set_default_icon(&mut self, scope: Scope, icon: Option<&IconResVariant>) -> Result<()> {
let key = self.open_scope_for_setter(scope)?; let key = self.open_scope_for_setter(scope)?;
@@ -217,7 +274,7 @@ impl ApplicationsKey {
} }
None => { None => {
// Delete shell and its all subkey. // Delete shell and its all subkey.
key.delete_subkey_all(Self::NAMEOF_DEFAULT_ICON_PART1)?; regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_DEFAULT_ICON_PART1)?;
} }
} }
@@ -226,6 +283,10 @@ impl ApplicationsKey {
const NAMEOF_FRIENDLY_APP_NAME: &str = "FriendlyAppName"; 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>> { pub fn get_friendly_app_name(&self, view: View) -> Result<Option<StrResVariant>> {
let key = self.open_view_for_getter(view)?; let key = self.open_view_for_getter(view)?;
// Get value of it // Get value of it
@@ -234,6 +295,8 @@ impl ApplicationsKey {
Ok(value.map(|v| StrResVariant::from(v.as_str()))) 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( pub fn set_friendly_app_name(
&mut self, &mut self,
scope: Scope, scope: Scope,
@@ -248,7 +311,7 @@ impl ApplicationsKey {
} }
None => { None => {
// Delete this key // Delete this key
key.delete_value(Self::NAMEOF_FRIENDLY_APP_NAME)?; regext::arbitrarily_delete_value(&key, Self::NAMEOF_FRIENDLY_APP_NAME)?;
} }
} }
@@ -257,6 +320,10 @@ impl ApplicationsKey {
const NAMEOF_SUPPORTED_TYPES: &str = "SupportedTypes"; 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>>> { pub fn get_supported_types(&self, view: View) -> Result<Option<Vec<concept::Ext>>> {
let key = self.open_view_for_getter(view)?; let key = self.open_view_for_getter(view)?;
// Get supported types subkey // Get supported types subkey
@@ -276,6 +343,8 @@ impl ApplicationsKey {
Ok(Some(exts)) 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( pub fn set_supported_types(
&mut self, &mut self,
scope: Scope, scope: Scope,
@@ -298,7 +367,7 @@ impl ApplicationsKey {
} }
None => { None => {
// Delete this subkey. // Delete this subkey.
key.delete_subkey_all(Self::NAMEOF_SUPPORTED_TYPES)?; regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_SUPPORTED_TYPES)?;
} }
} }
@@ -307,6 +376,7 @@ impl ApplicationsKey {
const NAMEOF_NO_OPEN_WITH: &str = "NoOpenWith"; 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> { pub fn get_no_open_with(&self, view: View) -> Result<bool> {
let key = self.open_view_for_getter(view)?; let key = self.open_view_for_getter(view)?;
match regext::try_get_value::<String, _>(&key, Self::NAMEOF_NO_OPEN_WITH)? { match regext::try_get_value::<String, _>(&key, Self::NAMEOF_NO_OPEN_WITH)? {
@@ -315,12 +385,17 @@ impl ApplicationsKey {
} }
} }
/// 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<()> { pub fn set_no_open_with(&mut self, scope: Scope, flag: bool) -> Result<()> {
let key = self.open_scope_for_setter(scope)?; let key = self.open_scope_for_setter(scope)?;
if flag { if flag {
key.set_value(Self::NAMEOF_NO_OPEN_WITH, &"")?; key.set_value(Self::NAMEOF_NO_OPEN_WITH, &"")?;
} else { } else {
key.delete_value(Self::NAMEOF_NO_OPEN_WITH)?; regext::arbitrarily_delete_value(&key, Self::NAMEOF_NO_OPEN_WITH)?;
} }
Ok(()) Ok(())
} }
+55 -15
View File
@@ -6,16 +6,27 @@ use crate::win32::{concept, regext};
use winreg::RegKey; use winreg::RegKey;
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE}; 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)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ExtKey { pub struct ExtKey {
ext: concept::Ext, ext: concept::Ext,
} }
impl ExtKey { impl ExtKey {
/// Creates a new `ExtKey` with the given file extension.
pub fn new(inner: concept::Ext) -> Self { pub fn new(inner: concept::Ext) -> Self {
Self { ext: inner } Self { ext: inner }
} }
/// Returns the inner file extension of this extension key.
pub fn inner(&self) -> &concept::Ext { pub fn inner(&self) -> &concept::Ext {
&self.ext &self.ext
} }
@@ -25,6 +36,7 @@ impl ExtKey {
const FULL_CLASSES: &str = "Software\\Classes"; const FULL_CLASSES: &str = "Software\\Classes";
const PARTIAL_CLASSES: &str = ""; 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> { fn open_key(&self, territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<OpenedKey> {
// check privilege // check privilege
check_privilege(territory, purpose)?; check_privilege(territory, purpose)?;
@@ -53,19 +65,27 @@ impl ExtKey {
Ok(OpenedKey::new(classes, this_ext)) 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> { fn open_view_for_read(&self, view: View) -> Result<OpenedKey> {
self.open_key(view.into(), OpenKeyPurpose::Read) 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> { fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey> {
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite) 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> { pub fn is_exist(&self, view: View) -> Result<bool> {
let key = self.open_view_for_read(view)?.this_key; let key = self.open_view_for_read(view)?.this_key;
Ok(key.is_some()) 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> { pub fn ensure(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?; let key = self.open_scope_for_write(scope)?;
if let None = key.this_key { if let None = key.this_key {
@@ -79,28 +99,34 @@ impl ExtKey {
} }
} }
pub fn delete(&mut self, scope: Scope) -> Result<()> { /// Delete this file extension key from Classes key.
///
/// Return true if we successfully delete this key,
/// otherwise false indicating there is no such key (already deleted).
pub fn delete(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?; let key = self.open_scope_for_write(scope)?;
key.parent_key Ok(regext::arbitrarily_delete_subkey_all(
.delete_subkey_all(regext::blank_path_guard(self.ext.dotted_inner())?)?; &key.parent_key,
Ok(()) regext::blank_path_guard(self.ext.dotted_inner())?,
)?)
} }
// YYC MARK: // YYC MARK:
// Reference: https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types#setting-optional-subkeys-and-file-type-extension-attributes // 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: // TODO:
// We do not support "Content Type" and "PerceivedType" // We only support these keys in there because current interface are enough to use.
// because current interface are enough to use,
// and these types has not been made as concept struct in Rust.
// We may expand these in future. // 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> { fn open_view_for_getter(&self, view: View) -> Result<RegKey> {
self.open_view_for_read(view)? self.open_view_for_read(view)?
.this_key .this_key
.ok_or(Error::InexistantKey) .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> { fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey> {
self.open_scope_for_write(scope)? self.open_scope_for_write(scope)?
.this_key .this_key
@@ -109,6 +135,10 @@ impl ExtKey {
const NAMEOF_DEFAULT: &str = ""; 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>> { pub fn get_default(&self, view: View) -> Result<Option<LosseProgId>> {
let key = self.open_view_for_getter(view)?; let key = self.open_view_for_getter(view)?;
// Get value of it // Get value of it
@@ -117,6 +147,8 @@ impl ExtKey {
Ok(value.map(|v| LosseProgId::from(v.as_str()))) 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<()> { pub fn set_default(&mut self, scope: Scope, pid: Option<&LosseProgId>) -> Result<()> {
let key = self.open_scope_for_setter(scope)?; let key = self.open_scope_for_setter(scope)?;
@@ -127,7 +159,7 @@ impl ExtKey {
} }
None => { None => {
// Delete this key // Delete this key
key.delete_value(Self::NAMEOF_DEFAULT)?; regext::arbitrarily_delete_value(&key, Self::NAMEOF_DEFAULT)?;
} }
} }
@@ -136,6 +168,11 @@ impl ExtKey {
const NAMEOF_OPEN_WITH_PROGIDS: &str = "OpenWithProgIds"; 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>>> { pub fn get_open_with_progids(&self, view: View) -> Result<Option<Vec<LosseProgId>>> {
let key = self.open_view_for_getter(view)?; let key = self.open_view_for_getter(view)?;
// Get OpenWithProgIds subkey // Get OpenWithProgIds subkey
@@ -156,7 +193,7 @@ impl ExtKey {
Ok(Some(progids)) 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. /// If there is no "OpenWithProgIds" subkey, this function return false.
pub fn is_in_open_with_progids(&self, view: View, pid: &LosseProgId) -> Result<bool> { pub fn is_in_open_with_progids(&self, view: View, pid: &LosseProgId) -> Result<bool> {
@@ -176,7 +213,7 @@ impl ExtKey {
.is_some()) .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, /// If there is no "OpenWithProgIds" subkey, this function will create it first,
/// then add your given ProgId into it. /// then add your given ProgId into it.
@@ -190,9 +227,9 @@ impl ExtKey {
Ok(()) Ok(())
} }
/// Removes the given ProgId from the "OpenWithProgIds" subkey under the given scope.
/// ///
/// /// If there is no "OpenWithProgIds" subkey, this function does nothing.
/// If there is no "OpenWithProgIds" subkey, this function do nothing.
pub fn remove_from_open_with_progids(&mut self, scope: Scope, pid: &LosseProgId) -> Result<()> { pub fn remove_from_open_with_progids(&mut self, scope: Scope, pid: &LosseProgId) -> Result<()> {
let key = self.open_scope_for_setter(scope)?; let key = self.open_scope_for_setter(scope)?;
// Try get subkey // Try get subkey
@@ -205,7 +242,10 @@ impl ExtKey {
None => return Ok(()), None => return Ok(()),
}; };
// Remove given key // Remove given key
open_with_progids_key.delete_value(pid.to_string())?; regext::arbitrarily_delete_value(
&open_with_progids_key,
regext::blank_path_guard(pid.to_string())?,
)?;
Ok(()) Ok(())
} }
} }
+88 -17
View File
@@ -6,16 +6,27 @@ use crate::win32::regext;
use winreg::RegKey; use winreg::RegKey;
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE}; 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)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ProgIdKey { pub struct ProgIdKey {
progid: LosseProgId, progid: LosseProgId,
} }
impl ProgIdKey { impl ProgIdKey {
/// Creates a new `ProgIdKey` with the given ProgId.
pub fn new(inner: LosseProgId) -> Self { pub fn new(inner: LosseProgId) -> Self {
Self { progid: inner } Self { progid: inner }
} }
/// Returns the inner ProgId of this ProgId key.
pub fn inner(&self) -> &LosseProgId { pub fn inner(&self) -> &LosseProgId {
&self.progid &self.progid
} }
@@ -25,6 +36,7 @@ impl ProgIdKey {
const FULL_CLASSES: &str = "Software\\Classes"; const FULL_CLASSES: &str = "Software\\Classes";
const PARTIAL_CLASSES: &str = ""; 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> { fn open_key(&self, territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<OpenedKey> {
// check privilege // check privilege
check_privilege(territory, purpose)?; check_privilege(territory, purpose)?;
@@ -53,19 +65,27 @@ impl ProgIdKey {
Ok(OpenedKey::new(classes, this_progid)) 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> { fn open_view_for_read(&self, view: View) -> Result<OpenedKey> {
self.open_key(view.into(), OpenKeyPurpose::Read) 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> { fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey> {
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite) 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> { pub fn is_exist(&self, view: View) -> Result<bool> {
let key = self.open_view_for_read(view)?.this_key; let key = self.open_view_for_read(view)?.this_key;
Ok(key.is_some()) 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> { pub fn ensure(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?; let key = self.open_scope_for_write(scope)?;
if let None = key.this_key { if let None = key.this_key {
@@ -79,27 +99,33 @@ impl ProgIdKey {
} }
} }
pub fn delete(&mut self, scope: Scope) -> Result<()> { /// Delete this ProgId key from Classes key.
///
/// Return true if we successfully delete this key,
/// otherwise false indicating there is no such key (already deleted).
pub fn delete(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?; let key = self.open_scope_for_write(scope)?;
key.parent_key Ok(regext::arbitrarily_delete_subkey_all(
.delete_subkey_all(regext::blank_path_guard(self.progid.to_string())?)?; &key.parent_key,
Ok(()) regext::blank_path_guard(self.progid.to_string())?,
)?)
} }
// YYC MARK: // YYC MARK:
// Reference: https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids#programmatic-identifier-elements-used-by-file-associations // Reference: <https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids#programmatic-identifier-elements-used-by-file-associations>
// TODO: // TODO:
// Currently we only support (Default), FriendlyTypeName and DefaultIcon // We only support these keys in there because current interface are enough to use.
// to just cover the basic usage.
// We may expand these in future. // 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> { fn open_view_for_getter(&self, view: View) -> Result<RegKey> {
self.open_view_for_read(view)? self.open_view_for_read(view)?
.this_key .this_key
.ok_or(Error::InexistantKey) .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> { fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey> {
self.open_scope_for_write(scope)? self.open_scope_for_write(scope)?
.this_key .this_key
@@ -108,6 +134,11 @@ impl ProgIdKey {
const NAMEOF_DEFAULT: &str = ""; 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>> { pub fn get_default(&self, view: View) -> Result<Option<StrResVariant>> {
let key = self.open_view_for_getter(view)?; let key = self.open_view_for_getter(view)?;
// Get value of it // Get value of it
@@ -116,9 +147,7 @@ impl ProgIdKey {
Ok(value.map(|v| StrResVariant::from(v.as_str()))) Ok(value.map(|v| StrResVariant::from(v.as_str())))
} }
/// /// Sets the default value (friendly name) for this ProgId under the given scope.
///
/// The legacy way to set friendly name for this ProgId.
pub fn set_default(&mut self, scope: Scope, name: Option<&StrResVariant>) -> Result<()> { pub fn set_default(&mut self, scope: Scope, name: Option<&StrResVariant>) -> Result<()> {
let key = self.open_scope_for_setter(scope)?; let key = self.open_scope_for_setter(scope)?;
@@ -129,17 +158,28 @@ impl ProgIdKey {
} }
None => { None => {
// Delete this key // Delete this key
key.delete_value(Self::NAMEOF_DEFAULT)?; regext::arbitrarily_delete_value(&key, Self::NAMEOF_DEFAULT)?;
} }
} }
Ok(()) Ok(())
} }
// TODO:
// Support mutiple ShellVerb in future.
const NAMEOF_SHELL_VERB_PART1: &str = "shell"; const NAMEOF_SHELL_VERB_PART1: &str = "shell";
const NAMEOF_SHELL_VERB_PART3: &str = "command"; const NAMEOF_SHELL_VERB_PART3: &str = "command";
const NAMEOF_SHELL_VERB_PART4: &str = ""; 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>> { pub fn get_shell_verb(&self, view: View) -> Result<Option<ShellVerb>> {
let key = self.open_view_for_getter(view)?; let key = self.open_view_for_getter(view)?;
@@ -190,6 +230,15 @@ impl ProgIdKey {
))) )))
} }
/// 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<()> { pub fn set_shell_verb(&mut self, scope: Scope, sv: Option<&ShellVerb>) -> Result<()> {
let key = self.open_scope_for_setter(scope)?; let key = self.open_scope_for_setter(scope)?;
@@ -209,15 +258,24 @@ impl ProgIdKey {
} }
None => { None => {
// Delete shell and its all subkey. // Delete shell and its all subkey.
key.delete_subkey_all(Self::NAMEOF_SHELL_VERB_PART1)?; regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_SHELL_VERB_PART1)?;
} }
} }
Ok(()) 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"; 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>> { pub fn get_friendly_type_name(&self, view: View) -> Result<Option<StrResVariant>> {
let key = self.open_view_for_getter(view)?; let key = self.open_view_for_getter(view)?;
// Get value of it // Get value of it
@@ -226,9 +284,8 @@ impl ProgIdKey {
Ok(value.map(|v| StrResVariant::from(v.as_str()))) 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.
/// Set this entry to a friendly name for the ProgID.
pub fn set_friendly_type_name( pub fn set_friendly_type_name(
&mut self, &mut self,
scope: Scope, scope: Scope,
@@ -243,16 +300,25 @@ impl ProgIdKey {
} }
None => { None => {
// Delete this key // Delete this key
key.delete_value(Self::NAMEOF_FRIENDLY_TYPE_NAME)?; regext::arbitrarily_delete_value(&key, Self::NAMEOF_FRIENDLY_TYPE_NAME)?;
} }
} }
Ok(()) 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_PART1: &str = "DefaultIcon";
const NAMEOF_DEFAULT_ICON_PART2: &str = ""; 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>> { pub fn get_default_icon(&self, view: View) -> Result<Option<IconResVariant>> {
let key = self.open_view_for_getter(view)?; let key = self.open_view_for_getter(view)?;
// Get default icon subkey // Get default icon subkey
@@ -271,6 +337,11 @@ impl ProgIdKey {
Ok(default_icon_default_value.map(|v| IconResVariant::from(v.as_str()))) 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<()> { pub fn set_default_icon(&mut self, scope: Scope, icon: Option<&IconResVariant>) -> Result<()> {
let key = self.open_scope_for_setter(scope)?; let key = self.open_scope_for_setter(scope)?;
@@ -284,7 +355,7 @@ impl ProgIdKey {
} }
None => { None => {
// Delete shell and its all subkey. // Delete shell and its all subkey.
key.delete_subkey_all(Self::NAMEOF_DEFAULT_ICON_PART1)?; regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_DEFAULT_ICON_PART1)?;
} }
} }
+6 -3
View File
@@ -5,11 +5,11 @@ use std::iter::FusedIterator;
use std::path::Path; use std::path::Path;
use thiserror::Error as TeError; 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)] #[derive(Debug, TeError)]
#[error("fail to cast OS string into string")] #[error("fail to cast OS string into Rust string")]
pub struct CastOsStrError {} pub struct CastOsStrError {}
impl CastOsStrError { impl CastOsStrError {
@@ -32,6 +32,8 @@ pub fn osstr_to_str(osstr: &OsStr) -> Result<&str, CastOsStrError> {
// region: Capitalize First ASCII Letter // 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> struct CapitalizeFirstAscii<T>
where where
T: Iterator<Item = char>, T: Iterator<Item = char>,
@@ -72,6 +74,7 @@ where
impl<T> FusedIterator for CapitalizeFirstAscii<T> where T: Iterator<Item = char> + FusedIterator {} 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 { pub fn capitalize_first_ascii(s: &str) -> String {
CapitalizeFirstAscii::new(s.chars()).collect() 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 concept;
pub mod utilities; pub mod utilities;
pub mod regext; pub mod regext;
+215 -28
View File
@@ -15,7 +15,7 @@ use windows_sys::Win32::UI::WindowsAndMessaging::HICON;
/// The error occurs when constructing Ext with bad body. /// The error occurs when constructing Ext with bad body.
#[derive(Debug, TeError)] #[derive(Debug, TeError)]
#[error("given file extension body \"{inner}\" is invalid")] #[error(r#"given file extension body "{inner}" is invalid"#)]
pub struct BadExtBodyError { pub struct BadExtBodyError {
/// The clone of string which is not a valid file extension body. /// The clone of string which is not a valid file extension body.
inner: String, inner: String,
@@ -32,6 +32,12 @@ impl BadExtBodyError {
/// The struct representing an file extension which must start with dot (`.`) /// The struct representing an file extension which must start with dot (`.`)
/// and followed by at least one arbitrary characters. /// 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)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Ext { pub struct Ext {
/// The body of file extension (excluding dot). /// The body of file extension (excluding dot).
@@ -42,6 +48,8 @@ impl Ext {
/// Create an new file extension. /// Create an new file extension.
/// ///
/// `body` is the body of file extension (excluding dot, such as `jpg`). /// `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`, /// If you want to create this struct with ordinary extension string like `.jpg`,
/// please use `Ext::from_str()` or `parse::<Ext>()` instead. /// please use `Ext::from_str()` or `parse::<Ext>()` instead.
pub fn new(body: &str) -> Result<Self, BadExtBodyError> { pub fn new(body: &str) -> Result<Self, BadExtBodyError> {
@@ -56,11 +64,17 @@ impl Ext {
} }
/// Get the body part of file extension (excluding dot) /// 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 { pub fn inner(&self) -> &str {
&self.body &self.body
} }
/// Get the body part of file extension (with leading dot) /// 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 { pub fn dotted_inner(&self) -> String {
// Reuse Display trait result // Reuse Display trait result
self.to_string() self.to_string()
@@ -69,7 +83,7 @@ impl Ext {
/// The error occurs when try parsing string into FileExt. /// The error occurs when try parsing string into FileExt.
#[derive(Debug, TeError)] #[derive(Debug, TeError)]
#[error("given file extension name \"{inner}\" is invalid")] #[error(r#"given file extension name "{inner}" is invalid"#)]
pub struct ParseExtError { pub struct ParseExtError {
/// The clone of string which is not a valid file extension. /// The clone of string which is not a valid file extension.
inner: String, inner: String,
@@ -110,7 +124,7 @@ impl FromStr for Ext {
/// The error occurs when constructing ProgId. /// The error occurs when constructing ProgId.
#[derive(Debug, TeError)] #[derive(Debug, TeError)]
#[error("given ProgId part \"{inner}\" is invalid")] #[error(r#"given ProgId part "{inner}" is invalid"#)]
pub struct BadProgIdPartError { pub struct BadProgIdPartError {
/// The clone of string which is not a valid ProgId part. /// The clone of string which is not a valid ProgId part.
inner: String, inner: String,
@@ -127,16 +141,21 @@ impl BadProgIdPartError {
/// The ProgId exactly follows Microsoft suggested /// The ProgId exactly follows Microsoft suggested
/// `[Vendor or Application].[Component].[Version]` format. /// `[Vendor or Application].[Component].[Version]` format.
/// And additionally, `[Version]` part is optional.
/// ///
/// Additionally, `[Version]` part is optional. /// In reality world, most of applications do no follow this standard.
/// /// However, this scenario is not convered by this struct in there.
/// However, most of applications do no follow this standard,
/// this scenario is not convered by this struct in there.
/// It should be done by other structs in other places. /// 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: /// Reference:
/// - https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids /// - <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/com/-progid--key>
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ProgId { pub struct ProgId {
/// The vendor part of ProgId. /// The vendor part of ProgId.
@@ -149,6 +168,9 @@ pub struct ProgId {
impl ProgId { impl ProgId {
/// Create a new ProgId with given parts. /// 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( pub fn new(
vendor: &str, vendor: &str,
component: &str, component: &str,
@@ -186,7 +208,7 @@ impl ProgId {
/// The error occurs when parsing ProgId. /// The error occurs when parsing ProgId.
#[derive(Debug, TeError)] #[derive(Debug, TeError)]
#[error("given ProgId \"{inner}\" is invalid")] #[error(r#"given ProgId "{inner}" is invalid"#)]
pub struct ParseProgIdError { pub struct ParseProgIdError {
/// The clone of string which is not a valid ProgId. /// The clone of string which is not a valid ProgId.
inner: String, inner: String,
@@ -245,6 +267,14 @@ impl FromStr for ProgId {
/// `{26EE0668-A00A-44D7-9371-BEB064C98683}` (case insensitive). /// `{26EE0668-A00A-44D7-9371-BEB064C98683}` (case insensitive).
/// ///
/// Please note that the curly brace is the essential part of CLSID. /// 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)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Clsid { pub struct Clsid {
inner: Uuid, inner: Uuid,
@@ -252,7 +282,7 @@ pub struct Clsid {
impl Clsid { impl Clsid {
/// Create new CLSID from underlying UUID. /// Create new CLSID from underlying UUID.
fn new(uuid: &Uuid) -> Self { pub fn new(uuid: &Uuid) -> Self {
Self { inner: *uuid } Self { inner: *uuid }
} }
@@ -264,7 +294,7 @@ impl Clsid {
/// The error occurs when parsing CLSID. /// The error occurs when parsing CLSID.
#[derive(Debug, TeError)] #[derive(Debug, TeError)]
#[error("given string \"{inner}\" is invalid for uuid")] #[error(r#"given string "{inner}" is invalid for CLSID"#)]
pub struct ParseClsidError { pub struct ParseClsidError {
/// The clone of string which is not a valid CLSID. /// The clone of string which is not a valid CLSID.
inner: String, inner: String,
@@ -301,7 +331,7 @@ impl Display for Clsid {
/// Error occurs when given string is not a valid Icon Reference String. /// Error occurs when given string is not a valid Icon Reference String.
#[derive(Debug, TeError)] #[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 { pub struct ParseIconRefStrError {
/// The clone of string which is not a valid Icon Reference String /// The clone of string which is not a valid Icon Reference String
inner: 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 /// The struct representing an Icon Reference String
/// looks like `%SystemRoot%\System32\imageres.dll,-72`. /// looks like `%SystemRoot%\System32\imageres.dll,-72`.
/// ///
/// As far as I know, the minus token `-` does nothing in this string. /// As far as I know, the minus token `-` does nothing in this string.
/// The following number is just the index. /// 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)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct IconRefStr { pub struct IconRefStr {
/// The path part of this reference string. /// The path part of this reference string.
@@ -352,6 +393,12 @@ impl IconRefStr {
/// Get the path part of this reference string. /// Get the path part of this reference string.
/// ///
/// This path can be absolute path, relative path or expandable path. /// 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 { pub fn get_path(&self) -> &str {
&self.path &self.path
} }
@@ -395,7 +442,7 @@ impl FromStr for IconRefStr {
/// Error occurs when given string is not a valid String Reference String. /// Error occurs when given string is not a valid String Reference String.
#[derive(Debug, TeError)] #[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 { pub struct ParseStrRefStrError {
/// The clone of string which is not a valid String Reference String /// The clone of string which is not a valid String Reference String
inner: 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 /// The struct representing an String Reference String
/// looks like `@%SystemRoot%\System32\shell32.dll,-30596`. /// looks like `@%SystemRoot%\System32\shell32.dll,-30596`.
/// ///
/// As far as I know, the minus token `-` does nothing in this string. /// As far as I know, the minus token `-` does nothing in this string.
/// The following number is just the index. /// 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)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StrRefStr { pub struct StrRefStr {
/// The path part of this reference string. /// The path part of this reference string.
@@ -440,6 +498,12 @@ impl StrRefStr {
/// Get the path part of this reference string. /// Get the path part of this reference string.
/// ///
/// This path can be absolute path, relative path or expandable path. /// 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 { pub fn get_path(&self) -> &str {
&self.path &self.path
} }
@@ -504,14 +568,40 @@ pub enum IconSizeKind {
Large, 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)] #[derive(Debug)]
pub struct IconRc { pub struct IconRc {
icon: HICON, 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 { impl IconRc {
/// Load icon from executable or `.ico` file. /// 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. /// 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. /// Otherwise `index` is the icon resource index located in executable.
@@ -548,16 +638,28 @@ impl IconRc {
Self::new(file, 0, kind) 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 { pub unsafe fn from_raw(hicon: HICON) -> Self {
Self { icon: hicon } Self { icon: hicon }
} }
pub fn into_raw(self) -> HICON { /// Consume and leak this struct held Win32 icon handle.
self.icon ///
/// 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 { 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 { pub fn get_icon(&self) -> HICON {
self.icon self.icon
} }
@@ -608,12 +710,18 @@ pub enum LoadStrRcError {
} }
/// The struct representing a loaded string resource. /// The struct representing a loaded string resource.
///
/// You can use [StrRc::new] to create this resource.
#[derive(Debug)] #[derive(Debug)]
pub struct StrRc { pub struct StrRc {
inner: String, inner: String,
} }
impl StrRc { 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> { pub fn new(file: &str, index: u32) -> Result<Self, LoadStrRcError> {
use windows_sys::Win32::Foundation::FreeLibrary; use windows_sys::Win32::Foundation::FreeLibrary;
use windows_sys::Win32::System::LibraryLoader::{ use windows_sys::Win32::System::LibraryLoader::{
@@ -659,10 +767,12 @@ impl StrRc {
} }
impl StrRc { impl StrRc {
/// Get fetched string resource.
pub fn get_string(&self) -> &str { pub fn get_string(&self) -> &str {
&self.inner &self.inner
} }
/// Consume and return fetched string resource.
pub fn into_string(self) -> String { pub fn into_string(self) -> String {
self.inner self.inner
} }
@@ -703,9 +813,15 @@ pub enum ExpandEnvVarError {
NoEnvVar, NoEnvVar,
} }
/// The struct representing an Expand String, /// The struct representing an expandable string,
/// which contain environment variable in string, /// which contain environment variable in string,
/// like `%LOCALAPPDATA%\SomeApp.exe`. /// 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)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ExpandString { pub struct ExpandString {
inner: String, inner: String,
@@ -723,7 +839,7 @@ impl ExpandString {
} }
/// Expand the variables located in this string /// Expand the variables located in this string
/// and produce the final usable string. /// and produce the final string without having any embedded variable syntax.
pub fn expand(&self) -> Result<String, ExpandEnvVarError> { pub fn expand(&self) -> Result<String, ExpandEnvVarError> {
use windows_sys::Win32::System::Environment::ExpandEnvironmentStringsW; use windows_sys::Win32::System::Environment::ExpandEnvironmentStringsW;
@@ -784,9 +900,15 @@ impl FromStr for ExpandString {
// region: File Name // region: File Name
/// The error occurs when constructing FileName with bad file name.
pub type BadFileNameError = ParseFileNameError; pub type BadFileNameError = ParseFileNameError;
/// The struct representing a legal Windows file name. /// 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)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FileName { pub struct FileName {
/// The validated legal Windows file name. /// The validated legal Windows file name.
@@ -810,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)] #[derive(Debug, TeError)]
#[error("given file name is illegal in Windows")] #[error("given file name is illegal in Windows")]
pub enum ParseFileNameError { pub enum ParseFileNameError {
@@ -857,9 +979,15 @@ impl FromStr for FileName {
// region: Verb // region: Verb
/// The error occurs when constructing Verb with bad verb name.
pub type BadVerbError = ParseVerbError; pub type BadVerbError = ParseVerbError;
/// The struct representing a verb when manipulating file /// 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)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Verb { pub struct Verb {
/// The validated verb name. /// The validated verb name.
@@ -867,16 +995,19 @@ pub struct Verb {
} }
impl Verb { impl Verb {
/// Get the verb representing Open.
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn OPEN() -> Self { pub fn OPEN() -> Self {
Verb::new("open").expect("unexpected bad verb") Verb::new("open").expect("unexpected bad verb")
} }
/// Get the verb representing Edit.
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn EDIT() -> Self { pub fn EDIT() -> Self {
Verb::new("edit").expect("unexpected bad verb") Verb::new("edit").expect("unexpected bad verb")
} }
/// Get the verb representing Play.
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn PLAY() -> Self { pub fn PLAY() -> Self {
Verb::new("play").expect("unexpected bad verb") Verb::new("play").expect("unexpected bad verb")
@@ -898,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)] #[derive(Debug, TeError)]
#[error("given verb \"{inner}\" is illegal")] #[error(r#"given verb "{inner}" is illegal"#)]
pub struct ParseVerbError { pub struct ParseVerbError {
inner: String, inner: String,
} }
@@ -939,52 +1070,106 @@ impl FromStr for Verb {
// region: Command Line // 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. /// The error occurs when constructing CmdLine with bad arguments.
#[derive(Debug, TeError)] #[derive(Debug, TeError)]
#[error("given file extension body \"{inner}\" is invalid")] #[error(r#"given command line argument "{inner}" is invalid"#)]
pub struct BadCmdLineError { pub struct BadCmdLineError {
inner: String, 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. /// The struct representing a Windows command line.
///
/// 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.
/// ///
/// # Note /// # Todo
/// ///
/// This struct currently does nothing for validation for given command line. /// This struct currently does nothing for validation for given command line.
/// Because there is no standard for validating this. /// Because there is no standard for validating this.
/// So I just write this struct as a placeholder for future extension. /// 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)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CmdLine { pub struct CmdLine {
inner: Vec<String>, inner: Vec<String>,
} }
impl CmdLine { 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> { pub fn new<S: AsRef<str>>(args: &[S]) -> Result<Self, BadCmdLineError> {
Ok(Self { Ok(Self {
inner: args.iter().map(|s| s.as_ref().to_string()).collect(), 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 { pub fn full(&self) -> String {
self.inner.join(" ") self.inner.join(" ")
} }
/// Get the iterator of command line arguments. /// 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> { pub fn iter(&self) -> impl Iterator<Item = &str> {
self.inner.iter().map(|s| s.as_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)] #[derive(Debug, TeError)]
#[error("given command line \"{inner}\" is invalid")] #[error(r#"given command line "{inner}" is invalid"#)]
pub struct ParseCmdLineError { pub struct ParseCmdLineError {
inner: String, inner: String,
} }
impl ParseCmdLineError { impl ParseCmdLineError {
// TODO: Remove this dead_code attribute once we need use this error type.
#[allow(dead_code)]
fn new(inner: &str) -> Self { fn new(inner: &str) -> Self {
Self { Self {
inner: inner.to_string(), inner: inner.to_string(),
@@ -1008,3 +1193,5 @@ impl FromStr for CmdLine {
}) })
} }
} }
// endregion
+80 -20
View File
@@ -1,8 +1,6 @@
//! This module extend `winreg` crate to make it more suit for the usage of this crate. //! This module extend `winreg` crate to make it more suit for the usage of this crate.
use std::ffi::OsStr; use std::ffi::OsStr;
use std::ops::Deref;
use std::ops::DerefMut;
use thiserror::Error as TeError; use thiserror::Error as TeError;
use windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND; use windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND;
use windows_sys::Win32::System::Registry::REG_SAM_FLAGS; use windows_sys::Win32::System::Registry::REG_SAM_FLAGS;
@@ -12,13 +10,13 @@ use winreg::types::FromRegValue;
// region: Extra Operations // 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(...)`, /// If error occurs when fetching given subkey, it return `Err(...)`,
/// otherwise, it will return `Ok(Some(...))` if subkey is existing, /// otherwise, it will return `Ok(Some(...))` if subkey is existing,
/// or `Ok(None)` if there is no suchsub key. /// 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. /// it differ "no such subkey" error and other access error.
pub fn try_open_subkey_with_flags<P: AsRef<OsStr>>( pub fn try_open_subkey_with_flags<P: AsRef<OsStr>>(
regkey: &RegKey, regkey: &RegKey,
@@ -37,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(...)`, /// If error occurs when fetching given key, it return `Err(...)`,
/// otherwise, it will return `Ok(Some(...))` if key is existing, /// otherwise, it will return `Ok(Some(...))` if key is existing,
/// or `Ok(None)` if there is no such key. /// 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. /// it differ "no such key" error and other access error.
pub fn try_get_value<T: FromRegValue, N: AsRef<OsStr>>( pub fn try_get_value<T: FromRegValue, N: AsRef<OsStr>>(
regkey: &RegKey, regkey: &RegKey,
@@ -61,13 +59,65 @@ 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. /// 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 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 there is no any subkey, or has multiple subkeys, return None instead.
/// If error occurs when fetching data, return Err(_). /// If error occurs when fetching data, return `Err(_)`.
/// ///
/// This is usually used for ShellVerb fetching. /// This is used as fetching only one ShellVerb.
pub fn get_sole_subkey_name(regkey: &RegKey) -> std::io::Result<Option<String>> { pub fn get_sole_subkey_name(regkey: &RegKey) -> std::io::Result<Option<String>> {
let mut subkey_enumerator = regkey.enum_keys(); let mut subkey_enumerator = regkey.enum_keys();
@@ -85,8 +135,8 @@ pub fn get_sole_subkey_name(regkey: &RegKey) -> std::io::Result<Option<String>>
} }
/// Get the name list of all "string" subkeys in given key. /// Get the name list of all "string" subkeys in given key.
/// ///
/// This is usually used for "OpenWithProgIds" subkey. /// This is used in "OpenWithProgIds" subkey.
pub fn get_all_string_subkey_names(regkey: &RegKey) -> std::io::Result<Vec<String>> { pub fn get_all_string_subkey_names(regkey: &RegKey) -> std::io::Result<Vec<String>> {
regkey regkey
.enum_values() .enum_values()
@@ -105,11 +155,14 @@ pub fn get_all_string_subkey_names(regkey: &RegKey) -> std::io::Result<Vec<Strin
} }
/// Delete all contents, including values and subkeys of given key. /// 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]. /// 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. /// 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. /// 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<()> { 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("") regkey.delete_subkey_all("")
} }
@@ -129,15 +182,15 @@ impl BlankPathError {
} }
/// Check whether given registry path is empty. /// 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. /// Passing empty path for some [winreg] functions is dangerous, expecially for those registry delete functions.
/// Because it will cause unexpected behavior that returning key self, rather than subkey. /// Passing empty path to registry creation function will return themselves,
/// This is VERY dangerous especially for those registry delete functions. /// 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 registry function. /// So I create this function to prevent any harmful blank path was passed into [winreg] function.
/// ///
/// This function MUST be used for the value, whose content can not be confirmed at compile time, /// This function MUST be used for wrapping the value, whose content can not be confirmed at compile time,
/// and it will be passed to get/set value, or create/delete key functions. /// 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> { pub fn blank_path_guard<P: AsRef<OsStr>>(path: P) -> std::result::Result<P, BlankPathError> {
if path.as_ref().is_empty() { if path.as_ref().is_empty() {
Err(BlankPathError::new()) Err(BlankPathError::new())
@@ -150,6 +203,11 @@ pub fn blank_path_guard<P: AsRef<OsStr>>(path: P) -> std::result::Result<P, Blan
// region: Expand String // 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, /// 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. /// to make they are different when use it with String as generic argument.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -217,4 +275,6 @@ impl From<&str> for ExpandString {
} }
} }
*/
// endregion // endregion
+6 -1
View File
@@ -7,7 +7,12 @@
/// It usually means that checking whether current process is running as Administrator. /// It usually means that checking whether current process is running as Administrator.
/// Return true if it is, otherwise false. /// 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 { pub fn has_privilege() -> bool {
use windows_sys::Win32::Foundation::HANDLE; use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Security::{ 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."
)
)
}
+11
View File
@@ -186,6 +186,9 @@ fn test_str_ref_str() {
#[test] #[test]
fn test_icon_rc() { fn test_icon_rc() {
fn const_tester(icon: Result<IconRc, LoadIconRcError>) {
assert!(icon.is_ok())
}
fn ok_tester(file: &str, index: u32) { fn ok_tester(file: &str, index: u32) {
let icon = IconRc::new(file, index, IconSizeKind::Small); let icon = IconRc::new(file, index, IconSizeKind::Small);
assert!(icon.is_ok()) assert!(icon.is_ok())
@@ -195,6 +198,9 @@ fn test_icon_rc() {
assert!(icon.is_err()) 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 // We pick it from "jpegfile" ProgId
ok_tester("imageres.dll", 72); ok_tester("imageres.dll", 72);
ok_tester("notepad.exe", 0); ok_tester("notepad.exe", 0);
@@ -312,12 +318,17 @@ fn test_verb() {
#[test] #[test]
fn test_cmd_line() { 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) { fn ok_tester(s: &str) {
let rv = CmdLine::from_str(s); let rv = CmdLine::from_str(s);
assert!(rv.is_ok()); assert!(rv.is_ok());
// let rv = rv.unwrap(); // let rv = rv.unwrap();
// assert_eq!(s, rv.full()); // assert_eq!(s, rv.full());
} }
#[allow(dead_code)]
fn err_tester(s: &str) { fn err_tester(s: &str) {
let rv = CmdLine::from_str(s); let rv = CmdLine::from_str(s);
assert!(rv.is_err()); 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