1
0

Compare commits

..

25 Commits

Author SHA1 Message Date
930a84a0f6 doc: add doc for test instruction 2026-06-01 22:08:25 +08:00
447d94fdd6 fix: add arbitrarily_delete_value and fix various bugs for lowlevel and highlevel 2026-05-29 09:46:59 +08:00
f0bd2c0b73 feat: add arbitrarily_delete_subkey_all for regext 2026-05-29 08:58:15 +08:00
119a4d0341 feat: add highlevel test 2026-05-28 21:34:42 +08:00
53b40a4d2f feat: add test for lowlevel 2026-05-28 21:03:50 +08:00
0533cdab23 test: add test for default icon resource 2026-05-27 19:12:52 +08:00
8c61aa1e1d feat: remove some optional in rust and cbindgen 2026-05-27 13:16:51 +08:00
77924b5937 feat: update cbindgen 2026-05-27 12:30:03 +08:00
2c811503a2 feat: update program and cdylib exposed interface 2026-05-27 10:38:45 +08:00
18a55272a3 ai: update tasks 2026-05-25 23:26:11 +08:00
1658127bdd doc: add some prompt for qwfassoc 2026-05-25 16:34:35 +08:00
72a8c13c1f feat: add c++ wfassoc wrapper 2026-05-19 20:34:12 +08:00
aececd8e5d feat: entirely remove old pure C project 2026-05-19 14:22:45 +08:00
883cba901c feat: updte wfassoc C header 2026-05-19 14:21:01 +08:00
a1874f3682 feat: update wfassoc C header 2026-05-19 12:54:59 +08:00
3c429b6c51 feat: remove all old useless code 2026-05-19 11:04:25 +08:00
a4d2f7bc26 feat: finish all highlevel function and expose them to cdylib 2026-05-19 10:59:57 +08:00
658806e9ff refactor: seperate highlevel into 2 individual files 2026-05-19 10:26:17 +08:00
f8db414da3 fix: fix the behavior of highlevel fetching string and icon 2026-05-18 21:26:02 +08:00
bc419f8d5e fix: fix refstr concept issue 2026-05-18 21:14:26 +08:00
3b0080849d fix: fix expand string error 2026-05-18 20:46:31 +08:00
53cc8edcfd fix: update blank guard usage and fix issue 2026-05-18 17:04:11 +08:00
1086039dcb feat: finish all lowlevel function 2026-05-18 16:40:06 +08:00
ae0231fe69 refactor: seperate lowlevel into 4 individual submodules 2026-05-18 16:27:39 +08:00
4205ef18d3 feat: write some content for lowlevel 2026-05-18 16:00:29 +08:00
49 changed files with 5515 additions and 5124 deletions

7
Cargo.lock generated
View File

@@ -480,6 +480,12 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "strfmt"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29fdc163db75f7b5ffa3daf0c5a7136fb0d4b2f35523cd1769da05e034159feb"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@@ -683,6 +689,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"regex", "regex",
"strfmt",
"thiserror", "thiserror",
"uuid", "uuid",
"widestring", "widestring",

View File

@@ -77,6 +77,7 @@ default = '@C:\path\to\ppic.exe,-1001'
# And more string resources... # And more string resources...
jpg = '@C:\path\to\ppic.exe,-1011' jpg = '@C:\path\to\ppic.exe,-1011'
jfif = '@C:\path\to\ppic.exe,-1050'
gif = '@C:\path\to\ppic.exe,-1012' gif = '@C:\path\to\ppic.exe,-1012'
bmp = '@C:\path\to\ppic.exe,-1013' bmp = '@C:\path\to\ppic.exe,-1013'
png = '@C:\path\to\ppic.exe,-1014' png = '@C:\path\to\ppic.exe,-1014'

View File

@@ -1,3 +0,0 @@
# Pineapple Picture Association
TODO

241
example/qwfassoc/PROMPT.txt Normal file
View File

@@ -0,0 +1,241 @@
我要求你使用Qt Widget编写一个GUI程序。该GUI程序是wfassoc的一个可视化界面。wfassoc是一个由Rust编写并暴露出C接口可以操作Windows注册表来管理应用程序的注册和卸载以及文件关联的动态链接库。
我要求你在@example/qwfassoc/TASKS.md 中先做好详细的规划,而不是上来就写代码。我会安排其他人来负责执行你做的规划。
# 项目要求
- 使用Qt Widget编写界面而不是QML。
- 使用UI文件而不是C++语句来构建界面。
- 我使用的是Qt 6使用CMake作为构建系统不要使用Qt的qmake。
- 使用toml11作为TOML读取库。
# 界面要求
这是一个基于 Qt Widgets 的标准对话框界面描述。你可以按照以下层级结构来构建代码:
## 主窗口容器 (Main Window)
* 类: `QDialog`。
* 窗口标题: "xxx选项"。xxx在应用程序初始化时通过wfassoc的Program提供的接口运行时获取。
* 窗口图标在应用程序初始化时通过wfassoc的Program提供的接口运行时获取。
* 大小限制固定大小480x600
## 选项卡 (Top Tabs)
* 组件: `QTabWidget`。
* 标签页 (Tabs): 从左到右依次添加以下标签页:
1. 应用程序
2. 文件关联
* 大小:选项卡占据对话框全部内容
## "应用程序"选项卡内容
该选项卡内部使用垂直布局 (`QVBoxLayout`),包含一个主要的分组区域:
### 区域内容
* 容器: `QGroupBox`。
* 区域标题:安装与卸载
* 布局: 垂直布局 (`QVBoxLayout`)。
* 上半部分
* 水平布局(`QHBoxLayout`)。
* 左侧: 一个 `QLabel` 显示图标该图标表示要设定的应用程序的图标。在应用程序初始化时通过wfassoc的Program提供的接口运行时获取。
* 右侧: 一个 `QLabel` 显示文本"在此安装或卸载xxx"表示要设定的应用程序的名称。xxx在应用程序初始化时通过wfassoc的Program提供的接口运行时获取。 (文本需要设置自动换行 `setWordWrap(true)`)。
* 下半部分
* 水平布局(`QHBoxLayout`)。
* 内容为两个 `QPushButton`,文本分别为:
* 安装:为当前对象(系统或当前用户,由应用程序初始化时从命令行参数获取)安装应用。如果应用程序已经安装,则不可点击。
* 卸载:为当前对象(系统或当前用户,由应用程序初始化时从命令行参数获取)卸载应用。如果应用程序没有安装,则不可点击。
## "文件关联"选项卡内容
在这个选项页内部,使用一个垂直布局 (`QVBoxLayout`) 来排列以下控件:
* 说明文本:
* 组件: `QLabel`。
* 文本: "使用 xxx 关联的文件类型:"。xxx在应用程序初始化时通过wfassoc的Program提供的接口运行时获取。
* 功能按钮行:
* 布局: `QHBoxLayout` (水平布局)。
* 组件: 两个 `QPushButton`。
* 文本: 两个按钮的文本都是 "+"。
* 位置: 位于列表上方,用于“全选”操作(第一次点击,将所有没关联的文件扩展名(显示为空白)设置为应用程序提供的打开方式。如果没有空白内容,或第二次点击,将所有文件全部设置为应用程序提供的打开方式)。
* 文件类型列表 (核心组件):
* 组件: `QTableWidget` (表格控件)。
* 列数: 3列。
* 表头 (Header):
* 第1列标题: "类型"
* 第2列标题: "uuu" (uuu在运行时进行获取为当前用户名)
* 第3列标题: "所有用户"
* 行内容示例:
* 第一列一个文件类型图标右边跟着对应的文本。表示当前文件扩展名和当前混合视图hybrid下的图标。图标和文本均从wfassoc函数获取。
* 第二列用户视图user下的名称。文本从wfassoc函数获取。
* 第三列系统视图system下的名称。文本从wfassoc函数获取。
* 滚动条: 右侧有一个垂直滚动条 (`QScrollBar`),表示内容超出可视区域。
* 操作方式:
* 第二列和第三列的元素可点击。
* 如果元素为空白或其它打开方式则点击后设置为当前应用程序指定的打开方式link
* 如果是自身打开方式点击后设置为空白unlink
* 点击后,第一列的图标需要改变,也因此你需要暂存当前应用程序提供打开方式的图标。如果第二第三列均为空,则不显示图标(仍然占位,显示为空白)。
* 点击操作并不会实时操作注册表,程序需要暂存用户的需求,然后在点击确认或应用按钮后再统一执行。
* 底部按钮栏
* 布局: `QHBoxLayout` (水平布局),通常右对齐或使用 `QDialogButtonBox`。
* 组件: 三个 `QPushButton`。
* 按钮文本 (从左到右):
1. "确定" (通常设为默认按钮 `setDefault(true)`)。
2. "取消"。
3. "应用":点击后应用修改,并留在页面 (如果没有修改,则不可用)。
额外注意:
* 如果应用程序没有安装,则本页面下所有内容均不启用。
* 如果启动时命令行指定以为当前用户安装的模式启动,则系统那一栏所有按钮都不可用
# 代码要求
- 有关wfassoc的接口请查阅@wfassoc-cdylib/codegen/wfassoc++.h 我要求你使用这个头文件中提供的内容来进行编写。
- wfassoc++.h文件中没有注释如果你想查看注释请访问@wfassoc-cdylib/codegen/wfassoc.h 文件。wfassoc++.h是wfassoc.h的C++包装。
- wfassoc.h所暴露的接口是由Rust编写的通常查看wfassoc.h可满足所有需求。如果仍有不确定的内容可查看其对应Rust项目的源码位于@wfassoc-cdylib/src 目录下。或更进一步地查看其依赖的源码,位于@wfassoc/src 目录下。
- 该GUI程序需要接受两个必要的命令行参数请使用Qt内置的命令行解析器进行解析
- `-c`或`--manifest`:指定要配置的应用程序的清单文件。
- `-f`或`--for`:指定应用程序要安装到的
- 清单文件的样例是@example/manifest/ppic.toml
- 在应用程序加载时或者执行操作时如果发生错误例如底层wfassoc发生错误丢失命令行选项等则弹出对话框报错然后立即退出程序
- 程序基本流程:
- 接受命令行,检查命令行参数是否合法
- 加载命令行指定的manifest文件并使用sanitizer检查错误。你可以阅读@wfassoc-exec/src/manifest.rs 文件来看看我是如何在Rust中检查它的。
- 按照给定的manifest文件使用wfassoc库构建schema然后再构建program。
- 初始化窗口。
- 调用wfassoc program提供的函数检查应用程序是否安装设置窗口的安装部分的按钮enable。
- 调用wfassoc program提供的函数遍历所有文件扩展名和关联情况设置窗口的文件关联表格。
- 用户可以在"应用程序"选项卡中注册和卸载应用程序点击后弹出窗口表示安装或卸载成功然后检测是否安装并刷新各个控件的enable状态。
- 用户可以在"文件关联"选项卡中设置是否以当前应用程序打开某些扩展名。用户可以点击全选按钮或单元格来进行设置,应用程序暂存修改,等用户点击确认或应用后再应用修改。如果点击的是应用,则刷新当前页面。
# 额外要求
- 不要尝试去编译来检查错误。我会安排其他人来检查程序是否能正常运行,并汇报回来,你再修改。
- 你不需要关心能否找到Qtwfassoc和toml11这些库。
- 对于wfassoc你只需要将@wfassoc-cdylib/codegen/Findwfassoc.cmake 复制到@example/qwfassoc/cmake 目录下并在此目录下编写一个README.md表明这个文件是从哪里复制来的即可。然后把复制的cmake文件所在目录加入find_package目录然后使用find_package寻找wfassoc即可。至于去哪里找这个库我会安排其他人来做。
- 对于Qt和toml11我会安排其他人来做你只需要用find_package来找他们就行不需要操心能不能找到。
- 如果你对某项需求有疑问,请问我,而不是进行猜测。
### 总结代码结构示意 (伪代码):
```cpp
QDialog *dialog = new QDialog();
dialog->setWindowTitle("选项");
QVBoxLayout *mainLayout = new QVBoxLayout(dialog);
// 1. Tab Widget
QTabWidget *tabWidget = new QTabWidget();
tabWidget->addTab(new QWidget(), "系统");
tabWidget->addTab(new QWidget(), "7-Zip");
// ... 其他 tabs
// 2. System Tab Content
QWidget *systemTab = tabWidget->widget(0);
QVBoxLayout *systemLayout = new QVBoxLayout(systemTab);
// Label
QLabel *label = new QLabel("使用 7-Zip 关联的文件类型:");
systemLayout->addWidget(label);
// Buttons (+)
QHBoxLayout *btnLayout = new QHBoxLayout();
QPushButton *btnPlus1 = new QPushButton("+");
QPushButton *btnPlus2 = new QPushButton("+");
btnLayout->addWidget(btnPlus1);
btnLayout->addWidget(btnPlus2);
systemLayout->addLayout(btnLayout);
// List (TreeWidget)
QTreeWidget *treeWidget = new QTreeWidget();
treeWidget->setColumnCount(3);
treeWidget->setHeaderLabels(QStringList() << "类型" << "yyc12345" << "所有用户");
// 添加 items...
systemLayout->addWidget(treeWidget);
// 3. Bottom Buttons
QHBoxLayout *bottomLayout = new QHBoxLayout();
bottomLayout->addStretch(); // 推挤按钮到右边
QPushButton *btnOK = new QPushButton("确定");
QPushButton *btnCancel = new QPushButton("取消");
QPushButton *btnApply = new QPushButton("应用(A)");
btnApply->setEnabled(false); // 禁用
QPushButton *btnHelp = new QPushButton("帮助");
bottomLayout->addWidget(btnOK);
bottomLayout->addWidget(btnCancel);
bottomLayout->addWidget(btnApply);
bottomLayout->addWidget(btnHelp);
mainLayout->addWidget(tabWidget);
mainLayout->addLayout(bottomLayout);
```
这是一个标准的 Windows 风格属性对话框,可以通过以下 Qt Widgets 结构来描述:
### 1. 主窗口容器
* 类: `QDialog`。
* 标题: "系统属性"。
* 布局: 垂直布局 (`QVBoxLayout`)。
### 2. 顶部选项卡 (Tabs)
* 组件: `QTabWidget`。
* 标签页: 包含 "计算机名", "硬件", "高级", "系统保护", "远程"。
* 当前选中: "硬件" 标签页。
### 3. "硬件" 选项卡内容
该选项卡内部使用垂直布局 (`QVBoxLayout`),包含两个主要的分组区域(视觉上类似 `QGroupBox` 或带有边框的 `QFrame`
#### 区域 A: 设备管理器 (上半部分)
* 容器: 一个带有边框的容器。
* 布局: 水平布局 (`QHBoxLayout`)。
* 左侧: 一个 `QLabel` 显示电脑图标。
* 中间: 一个 `QLabel` 显示说明文本:"设备管理器列出所有安装在计算机上的硬件设备。请使用设备管理器来更改设备的属性。" (文本需要设置自动换行 `setWordWrap(true)`)。
* 右侧/底部: 一个 `QPushButton`,文本为 "设备管理器(D)"。
#### 区域 B: 设备安装设置 (下半部分)
* 容器: 一个带有边框的容器。
* 布局: 垂直布局 (`QVBoxLayout`)。
* 顶部行: 水平布局 (`QHBoxLayout`)。
* 左侧: 一个 `QLabel` 显示列表/勾选图标。
* 右侧: 一个 `QLabel` 显示说明文本:"选择 Windows 是否下载制造商提供的可用于你的设备的应用和自定义图标。" (文本需要设置自动换行)。
* 底部: 一个 `QPushButton`,文本为 "设备安装设置(S)",靠右对齐。
### 4. 底部按钮栏
* 布局: 水平布局 (`QHBoxLayout`),右对齐 (通常通过 `addStretch()` 实现)。
* 组件: 三个 `QPushButton`。
* "确定"
* "取消"
* "应用(A)" (注意:截图中该按钮呈灰色,代码中需设置 `setEnabled(false)`)。
你制定的计划有一些问题,请按照下述标出的问题一一修正:
- 编写的代码和说明文件需要使用英文注释。
- 是manifest而非manifesto表示清单文件请修正这个拼写错误。
- 我看到你在mainwindow篇章中编写了大量的C++代码这没有必要。你是计划者而非执行者。你需要把需要在这个头文件中实现什么该怎么做需要使用哪些wfassoc函数这些函数该怎么调用在哪里查看他们怎么调用详细的告诉将要执行这些任务的执行者而不是直接为他们编写好代码。你在manifest部分的任务规划就非常符合这种范式。

View File

@@ -0,0 +1,3 @@
# Q WFAssoc
TODO

1313
example/qwfassoc/TASKS.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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}")

View File

@@ -0,0 +1,5 @@
# cmake 模块说明
此目录下的 `Findwfassoc.cmake` 是从项目根目录 `wfassoc-cdylib/codegen/Findwfassoc.cmake` 复制而来。
该文件提供 `wfassoc::wfassoc` imported target。使用前需要设置 `wfassoc_ROOT` 变量指向 wfassoc 安装目录。

405
legacy/.gitignore vendored
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

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

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

View File

@@ -1,9 +0,0 @@
LIBRARY wfassoc
EXPORTS
WFInstallApplicationW
WFInstallApplicationA
WFUninstallApplicationW
WFUninstallApplicationA
WFGenerateProgIDW
WFGenerateProgIDA

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>

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>

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))
);
}

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

View File

@@ -1 +0,0 @@
#include "wfassoc_private.h"

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

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

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

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);
}

View File

@@ -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>

View File

@@ -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>

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()

View File

@@ -0,0 +1,290 @@
/**
* @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>
namespace wfassocpp {
using wfassoc::CStyleString;
using wfassoc::Token;
using wfassoc::HICON;
using wfassoc::INVALID_HICON;
using wfassoc::INVALID_INDEX;
using wfassoc::Scope;
using wfassoc::View;
/// @private
inline void _Check(bool result) {
if (!result) {
throw std::runtime_error(wfassoc::WFGetLastError());
}
}
/// @private
inline Token _INVALID_TOKEN() {
static Token v = wfassoc::WFInvalidToken();
return v;
}
class Schema {
public:
Schema() { _Check(wfassoc::WFSchemaCreate(&_token)); }
~Schema() {
if (_token != _INVALID_TOKEN()) {
wfassoc::WFSchemaDestroy(_token);
}
}
Schema(const Schema&) = delete;
Schema& operator=(const Schema&) = delete;
Schema(Schema&& other) noexcept : _token(other._token) { other._token = _INVALID_TOKEN(); }
Schema& operator=(Schema&& other) noexcept {
if (this != &other) {
if (_token != _INVALID_TOKEN()) {
wfassoc::WFSchemaDestroy(_token);
}
_token = other._token;
other._token = _INVALID_TOKEN();
}
return *this;
}
void SetIdentifier(const char* value) { _Check(wfassoc::WFSchemaSetIdentifier(_token, value)); }
void SetPath(const char* value) { _Check(wfassoc::WFSchemaSetPath(_token, value)); }
void SetClsid(const char* value) { _Check(wfassoc::WFSchemaSetClsid(_token, value)); }
void SetName(const char* value) { _Check(wfassoc::WFSchemaSetName(_token, value)); }
void SetIcon(const char* value) { _Check(wfassoc::WFSchemaSetIcon(_token, value)); }
void SetBehavior(const char* value) { _Check(wfassoc::WFSchemaSetBehavior(_token, value)); }
void AddStr(const char* name, const char* value) { _Check(wfassoc::WFSchemaAddStr(_token, name, value)); }
void AddIcon(const char* name, const char* value) { _Check(wfassoc::WFSchemaAddIcon(_token, name, value)); }
void AddBehavior(const char* name, const char* value) { _Check(wfassoc::WFSchemaAddBehavior(_token, name, value)); }
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
Token Release() noexcept {
Token t = _token;
_token = _INVALID_TOKEN();
return t;
}
Token _token;
};
class IconRc {
public:
explicit IconRc(Token token) : _token(token) {}
~IconRc() {
if (_token != _INVALID_TOKEN()) {
wfassoc::WFIconRcDestroy(_token);
}
}
IconRc(const IconRc&) = delete;
IconRc& operator=(const IconRc&) = delete;
IconRc(IconRc&& other) noexcept : _token(other._token) { other._token = _INVALID_TOKEN(); }
IconRc& operator=(IconRc&& other) noexcept {
if (this != &other) {
if (_token != _INVALID_TOKEN()) {
wfassoc::WFIconRcDestroy(_token);
}
_token = other._token;
other._token = _INVALID_TOKEN();
}
return *this;
}
HICON GetIcon() {
HICON icon = nullptr;
_Check(wfassoc::WFIconRcGetIcon(_token, &icon));
return icon;
}
private:
Token _token;
};
class ExtStatus {
public:
explicit ExtStatus(Token token) : _token(token) {}
~ExtStatus() {
if (_token != _INVALID_TOKEN()) {
wfassoc::WFExtStatusDestroy(_token);
}
}
ExtStatus(const ExtStatus&) = delete;
ExtStatus& operator=(const ExtStatus&) = delete;
ExtStatus(ExtStatus&& other) noexcept : _token(other._token) { other._token = _INVALID_TOKEN(); }
ExtStatus& operator=(ExtStatus&& other) noexcept {
if (this != &other) {
if (_token != _INVALID_TOKEN()) {
wfassoc::WFExtStatusDestroy(_token);
}
_token = other._token;
other._token = _INVALID_TOKEN();
}
return *this;
}
std::string GetName() {
const char* name = nullptr;
_Check(wfassoc::WFExtStatusGetName(_token, &name));
return std::string(name);
}
HICON GetIcon() {
HICON icon = nullptr;
_Check(wfassoc::WFExtStatusGetIcon(_token, &icon));
return icon;
}
private:
Token _token;
};
class SelfExtStatus {
public:
explicit SelfExtStatus(Token token) : _token(token) {}
~SelfExtStatus() {
if (_token != _INVALID_TOKEN()) {
wfassoc::WFSelfExtStatusDestroy(_token);
}
}
SelfExtStatus(const SelfExtStatus&) = delete;
SelfExtStatus& operator=(const SelfExtStatus&) = delete;
SelfExtStatus(SelfExtStatus&& other) noexcept : _token(other._token) { other._token = _INVALID_TOKEN(); }
SelfExtStatus& operator=(SelfExtStatus&& other) noexcept {
if (this != &other) {
if (_token != _INVALID_TOKEN()) {
wfassoc::WFSelfExtStatusDestroy(_token);
}
_token = other._token;
other._token = _INVALID_TOKEN();
}
return *this;
}
std::string GetName() {
const char* name = nullptr;
_Check(wfassoc::WFSelfExtStatusGetName(_token, &name));
return std::string(name);
}
HICON GetIcon() {
HICON icon = nullptr;
_Check(wfassoc::WFSelfExtStatusGetIcon(_token, &icon));
return icon;
}
std::string GetExt() {
const char* inner = nullptr;
_Check(wfassoc::WFSelfExtStatusGetExt(_token, &inner));
return std::string(inner);
}
std::string GetDottedExt() {
const char* inner = nullptr;
_Check(wfassoc::WFSelfExtStatusGetDottedExt(_token, &inner));
return std::string(inner);
}
private:
Token _token;
};
class Program {
public:
explicit Program(Schema&& schema) {
_Check(wfassoc::WFProgramCreate(schema.Release(), &_token));
}
~Program() {
if (_token != _INVALID_TOKEN()) {
wfassoc::WFProgramDestroy(_token);
}
}
Program(const Program&) = delete;
Program& operator=(const Program&) = delete;
Program(Program&& other) noexcept : _token(other._token) { other._token = _INVALID_TOKEN(); }
Program& operator=(Program&& other) noexcept {
if (this != &other) {
if (_token != _INVALID_TOKEN()) {
wfassoc::WFProgramDestroy(_token);
}
_token = other._token;
other._token = _INVALID_TOKEN();
}
return *this;
}
std::string ResolveName() {
const char* name = nullptr;
_Check(wfassoc::WFProgramResolveName(_token, &name));
return std::string(name);
}
IconRc ResolveIcon() {
Token token = _INVALID_TOKEN();
_Check(wfassoc::WFProgramResolveIcon(_token, &token));
return IconRc(token);
}
size_t ExtsLen() {
size_t len = 0;
_Check(wfassoc::WFProgramExtsLen(_token, &len));
return len;
}
size_t FindExt(const char* body) {
size_t index = INVALID_INDEX;
_Check(wfassoc::WFProgramFindExt(_token, body, &index));
return index;
}
SelfExtStatus ResolveExt(size_t index) {
Token token = _INVALID_TOKEN();
_Check(wfassoc::WFProgramResolveExt(_token, index, &token));
return SelfExtStatus(token);
}
void Register(Scope scope) { _Check(wfassoc::WFProgramRegister(_token, scope)); }
void Unregister(Scope scope) { _Check(wfassoc::WFProgramUnregister(_token, scope)); }
bool IsRegistered(Scope scope) {
bool result = false;
_Check(wfassoc::WFProgramIsRegistered(_token, scope, &result));
return result;
}
void LinkExt(Scope scope, size_t index) { _Check(wfassoc::WFProgramLinkExt(_token, scope, index)); }
void UnlinkExt(Scope scope, size_t index) { _Check(wfassoc::WFProgramUnlinkExt(_token, scope, index)); }
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_

View File

@@ -0,0 +1,589 @@
/**
* @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
namespace wfassoc {
#endif // __cplusplus
#ifdef __cplusplus
/** Type representing a null-terminated UTF-8 C-style string */
using CStyleString = 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 Token = uint64_t;
/**
* @brief Type representing an icon handle (opaque pointer)
*
* This type is equivalent with Win32 HICON type.
*/
using HICON = void*;
#else // __cplusplus
typedef const char *CStyleString;
typedef uint64_t Token;
typedef void *HICON;
#endif // __cplusplus
#ifdef __cplusplus
/** Invalid icon handle value */
constexpr HICON INVALID_HICON = nullptr;
/** Invalid index value used for error conditions */
constexpr size_t INVALID_INDEX = static_cast<size_t>(-1);
#else // __cplusplus
static const HICON INVALID_HICON = NULL;
static const size_t 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 Scope : 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 View : 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 Scope;
/** Current user scope */
static const Scope SCOPE_USER = 0u;
/** System-wide scope */
static const Scope SCOPE_SYSTEM = 1u;
typedef uint32_t View;
/** User-level view */
static const View VIEW_USER = 0u;
/** System-level view */
static const View VIEW_SYSTEM = 1u;
/** Combined hybrid view of both user and system */
static const View 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.
*/
CStyleString 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
*/
Token WFInvalidToken(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(Token *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(Token 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(Token in_schema, CStyleString 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(Token in_schema, CStyleString 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(Token in_schema, CStyleString 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(Token in_schema, CStyleString 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(Token in_schema, CStyleString 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(Token in_schema, CStyleString 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(Token in_schema, CStyleString in_name, CStyleString 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(Token in_schema, CStyleString in_name, CStyleString 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(Token in_schema, CStyleString in_name, CStyleString 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(Token in_schema,
CStyleString in_ext,
CStyleString in_ext_name,
CStyleString in_ext_icon,
CStyleString 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(Token in_schema, Token *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(Token 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(Token in_program, CStyleString *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(Token in_program, Token *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(Token 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 INVALID_INDEX if not found.
* @return true on success, false on failure
*/
bool WFProgramFindExt(Token in_program, CStyleString 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(Token in_program, size_t in_index, Token *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(Token in_program, Scope 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(Token in_program, Scope 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(Token in_program, Scope 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(Token in_program, Scope 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(Token in_program, Scope 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(Token in_program, View in_view, size_t in_index, Token *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(Token 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(Token in_ext_status, CStyleString *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(Token in_ext_status, HICON *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(Token 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(Token in_self_ext_status, CStyleString *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(Token in_self_ext_status, HICON *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(Token in_self_ext_status, CStyleString *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(Token in_self_ext_status, CStyleString *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(Token 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 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(Token in_icon_rc, HICON *out_icon);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
#ifdef __cplusplus
} // namespace wfassoc
#endif // __cplusplus
#endif // WFASSOC_H_

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__

View File

@@ -1,8 +1,15 @@
//! The module including all FFI types used by this crate. //! The module including all FFI types used by this crate, except string type.
//! For string type, see also [crate::cstr_ffi].
use std::ffi::c_void; use std::ffi::c_void;
use num_enum::TryFromPrimitive; use num_enum::TryFromPrimitive;
// region: File Extension Index
pub const INVALID_INDEX: usize = usize::MAX;
// endregion
// region: HICON // region: HICON
/// The type representing Win32 HICON handle. /// The type representing Win32 HICON handle.
@@ -17,7 +24,7 @@ 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 [HICON] to re-define it in there.
pub const INVALID_ICON: HICON = std::ptr::null_mut(); pub const INVALID_HICON: HICON = std::ptr::null_mut();
// endregion // endregion

View File

@@ -1,7 +1,7 @@
mod cstr_ffi; mod cstr_ffi;
mod ffi_types;
mod last_error; mod last_error;
mod object_pool; mod object_pool;
mod ffi_types;
use object_pool::ObjectPool; use object_pool::ObjectPool;
use std::sync::{LazyLock, RwLock}; use std::sync::{LazyLock, RwLock};
@@ -24,17 +24,17 @@ enum Error {
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,
} }
@@ -188,7 +188,14 @@ static SCHEMA_POOL: LazyLock<RwLock<ObjectPool<Schema>>> =
static PROGRAM_POOL: LazyLock<RwLock<ObjectPool<Program>>> = static PROGRAM_POOL: LazyLock<RwLock<ObjectPool<Program>>> =
LazyLock::new(|| RwLock::new(ObjectPool::new())); LazyLock::new(|| RwLock::new(ObjectPool::new()));
static EXT_STATUE_POOL: LazyLock<RwLock<ObjectPool<wfassoc::highlevel::ProgramExtStatus>>> = static EXT_STATUS_POOL: LazyLock<RwLock<ObjectPool<wfassoc::highlevel::ProgramExtStatus>>> =
LazyLock::new(|| RwLock::new(ObjectPool::new()));
static SELF_EXT_STATUS_POOL: LazyLock<
RwLock<ObjectPool<wfassoc::highlevel::ProgramSelfExtStatus>>,
> = LazyLock::new(|| RwLock::new(ObjectPool::new()));
static ICON_RC_POOL: LazyLock<RwLock<ObjectPool<wfassoc::win32::concept::IconRc>>> =
LazyLock::new(|| RwLock::new(ObjectPool::new())); LazyLock::new(|| RwLock::new(ObjectPool::new()));
// endregion // endregion
@@ -196,8 +203,8 @@ static EXT_STATUE_POOL: LazyLock<RwLock<ObjectPool<wfassoc::highlevel::ProgramEx
// region: Exposed Types // region: Exposed Types
pub use cstr_ffi::CStyleString; pub use cstr_ffi::CStyleString;
pub use object_pool::Token;
pub use ffi_types::{HICON, Scope, View}; pub use ffi_types::{HICON, Scope, View};
pub use object_pool::Token;
// endregion // endregion
@@ -211,7 +218,9 @@ pub extern "C" fn WFStartup() -> bool {
cffi_wrapper!(|| { cffi_wrapper!(|| {
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_STATUE_POOL)?; let _pool = pull_writer!(EXT_STATUS_POOL)?;
let _pool = pull_writer!(SELF_EXT_STATUS_POOL)?;
let _pool = pull_writer!(ICON_RC_POOL)?;
Ok(()) Ok(())
}) })
} }
@@ -224,7 +233,11 @@ pub extern "C" fn WFShutdown() -> bool {
pool.clear(); pool.clear();
let mut pool = pull_writer!(PROGRAM_POOL)?; let mut pool = pull_writer!(PROGRAM_POOL)?;
pool.clear(); pool.clear();
let mut pool = pull_writer!(EXT_STATUE_POOL)?; let mut pool = pull_writer!(EXT_STATUS_POOL)?;
pool.clear();
let mut pool = pull_writer!(SELF_EXT_STATUS_POOL)?;
pool.clear();
let mut pool = pull_writer!(ICON_RC_POOL)?;
pool.clear(); pool.clear();
Ok(()) Ok(())
}) })
@@ -470,6 +483,84 @@ pub extern "C" fn WFProgramDestroy(in_program: in_param_ty!(Token)) -> bool {
}) })
} }
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramResolveName(
in_program: in_param_ty!(Token),
out_name: out_param_ty!(CStyleString),
) -> bool {
cffi_wrapper!(|in_program: Token| -> (out_name: CStyleString) {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
let name = program.resolve_name()?;
cstr_ffi::set_ffi_string(&name)?;
Ok(cstr_ffi::get_ffi_string())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramResolveIcon(
in_program: in_param_ty!(Token),
out_icon_rc: out_param_ty!(Token),
) -> bool {
cffi_wrapper!(|in_program: Token| -> (out_icon_rc: Token) {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
let icon = program.resolve_icon()?;
let mut pool = pull_writer!(ICON_RC_POOL)?;
Ok(pool.allocate(icon)?)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramExtsLen(
in_program: in_param_ty!(Token),
out_len: out_param_ty!(usize),
) -> bool {
cffi_wrapper!(|in_program: Token| -> (out_len: usize) {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
Ok(program.exts_len())
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramFindExt(
in_program: in_param_ty!(Token),
in_body: in_param_ty!(CStyleString),
out_index: out_param_ty!(usize),
) -> bool {
cffi_wrapper!(|in_program: Token, in_body: CStyleString| -> (out_index: usize) {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
let body = cstr_ffi::parse_ffi_string(in_body)?;
let index = match program.find_ext(body) {
Some(index) => index,
None => ffi_types::INVALID_INDEX,
};
Ok(index)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFProgramResolveExt(
in_program: in_param_ty!(Token),
in_index: in_param_ty!(usize),
out_self_ext_status: out_param_ty!(Token),
) -> bool {
cffi_wrapper!(|in_program: Token, in_index: usize| -> (out_self_ext_status: Token) {
let mut pool = pull_writer!(PROGRAM_POOL)?;
let program = pool.get_mut(in_program)?;
let self_ext_status = program.resolve_ext(in_index)?;
let mut pool = pull_writer!(SELF_EXT_STATUS_POOL)?;
let token = pool.allocate(self_ext_status)?;
Ok(token)
})
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn WFProgramRegister( pub extern "C" fn WFProgramRegister(
in_program: in_param_ty!(Token), in_program: in_param_ty!(Token),
@@ -557,7 +648,7 @@ pub extern "C" fn WFProgramQueryExt(
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 {
Some(ext_status) => { Some(ext_status) => {
let mut pool = pull_writer!(EXT_STATUE_POOL)?; let mut pool = pull_writer!(EXT_STATUS_POOL)?;
pool.allocate(ext_status)? pool.allocate(ext_status)?
}, },
None => object_pool::invalid_token(), None => object_pool::invalid_token(),
@@ -568,10 +659,12 @@ pub extern "C" fn WFProgramQueryExt(
// endregion // endregion
// 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!(Token)) -> bool {
cffi_wrapper!(|in_ext_status: Token| { cffi_wrapper!(|in_ext_status: Token| {
let mut pool = pull_writer!(EXT_STATUE_POOL)?; let mut pool = pull_writer!(EXT_STATUS_POOL)?;
Ok(pool.free(in_ext_status)?) Ok(pool.free(in_ext_status)?)
}) })
} }
@@ -582,7 +675,7 @@ pub extern "C" fn WFExtStatusGetName(
out_name: out_param_ty!(CStyleString), out_name: out_param_ty!(CStyleString),
) -> bool { ) -> bool {
cffi_wrapper!(|in_ext_status: Token| -> (out_name: CStyleString) { cffi_wrapper!(|in_ext_status: Token| -> (out_name: CStyleString) {
let pool = pull_reader!(EXT_STATUE_POOL)?; let pool = pull_reader!(EXT_STATUS_POOL)?;
let ext_status = pool.get(in_ext_status)?; let ext_status = pool.get(in_ext_status)?;
cstr_ffi::set_ffi_string(ext_status.get_name())?; cstr_ffi::set_ffi_string(ext_status.get_name())?;
@@ -596,14 +689,104 @@ pub extern "C" fn WFExtStatusGetIcon(
out_icon: out_param_ty!(HICON), out_icon: out_param_ty!(HICON),
) -> bool { ) -> bool {
cffi_wrapper!(|in_ext_status: Token| -> (out_icon: HICON) { cffi_wrapper!(|in_ext_status: Token| -> (out_icon: HICON) {
let pool = pull_reader!(EXT_STATUE_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_ICON, })
}; }
Ok(icon)
// endregion
// region: Self Extension Status
#[unsafe(no_mangle)]
pub extern "C" fn WFSelfExtStatusDestroy(in_self_ext_status: in_param_ty!(Token)) -> bool {
cffi_wrapper!(|in_self_ext_status: Token| {
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!(Token),
out_name: out_param_ty!(CStyleString),
) -> bool {
cffi_wrapper!(|in_self_ext_status: Token| -> (out_name: CStyleString) {
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!(Token),
out_icon: out_param_ty!(HICON),
) -> bool {
cffi_wrapper!(|in_self_ext_status: Token| -> (out_icon: HICON) {
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!(Token),
out_inner: out_param_ty!(CStyleString),
) -> bool {
cffi_wrapper!(|in_self_ext_status: Token| -> (out_inner: CStyleString) {
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!(Token),
out_inner: out_param_ty!(CStyleString),
) -> bool {
cffi_wrapper!(|in_self_ext_status: Token| -> (out_inner: CStyleString) {
let pool = pull_reader!(SELF_EXT_STATUS_POOL)?;
let self_ext_status = pool.get(in_self_ext_status)?;
cstr_ffi::set_ffi_string(self_ext_status.get_dotted_ext().as_str())?;
Ok(cstr_ffi::get_ffi_string())
})
}
// endregion
// region: Icon Resource
#[unsafe(no_mangle)]
pub extern "C" fn WFIconRcDestroy(in_icon_rc: in_param_ty!(Token)) -> bool {
cffi_wrapper!(|in_icon_rc: Token| {
let mut pool = pull_writer!(ICON_RC_POOL)?;
Ok(pool.free(in_icon_rc)?)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn WFIconRcGetIcon(
in_icon_rc: in_param_ty!(Token),
out_icon: out_param_ty!(HICON),
) -> bool {
cffi_wrapper!(|in_icon_rc: Token| -> (out_icon: HICON) {
let pool = pull_reader!(ICON_RC_POOL)?;
let icon_rc = pool.get(in_icon_rc)?;
Ok(icon_rc.get_icon())
}) })
} }

View File

@@ -65,7 +65,7 @@ fn stringified_exts_to_indices(
// 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.
if has_star { if has_star {
return Ok((0..program.get_ext_count()).collect()); return Ok((0..program.exts_len()).collect());
} }
// Convert each extension name to index using program.find_ext() // Convert each extension name to index using program.find_ext()
@@ -138,10 +138,13 @@ fn run_ext_list(
) -> Result<()> { ) -> Result<()> {
// 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.get_ext_count() { 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
@@ -168,7 +171,6 @@ fn run_ext_list(
pub fn run(c: cli::Cli) -> Result<()> { pub fn run(c: cli::Cli) -> Result<()> {
// Read manifest file first // Read manifest file first
let mf = manifest::Manifest::from_file(Path::new(&c.manifest_file))?; let mf = manifest::Manifest::from_file(Path::new(&c.manifest_file))?;
println!("{:?}", mf);
// Parse it into schema // Parse it into schema
let schema = mf.into_schema()?; let schema = mf.into_schema()?;
// Parse it into program // Parse it into program

View File

@@ -22,3 +22,6 @@ widestring = "1.2.1"
indexmap = "2.11.4" indexmap = "2.11.4"
regex = "1.11.3" regex = "1.11.3"
uuid = { version = "1.18.1", features = ["v4"] } uuid = { version = "1.18.1", features = ["v4"] }
[dev-dependencies]
strfmt = "0.2.5"

View File

@@ -1,5 +1,7 @@
# 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.
So programmers can directly use them. So programmers can directly use them.
@@ -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 highlt 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.

View File

@@ -1,391 +0,0 @@
//! The module including all struct representing Windows file association concept,
//! like file extension, ProgId, CLSID and etc.
use crate::extra::windows::{Ext, ProgId};
use crate::extra::winreg as winreg_extra;
use crate::utilities;
use std::fmt::Display;
use std::str::FromStr;
use thiserror::Error as TeError;
use winreg::RegKey;
use winreg::enums::{
HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE,
};
// region: Error Types
/// All possible error occurs in this crate.
#[derive(Debug, TeError)]
pub enum Error {
#[error("error occurs when manipulating with Registry: {0}")]
BadRegOper(#[from] std::io::Error),
#[error("{0}")]
ParseExt(#[from] crate::extra::windows::ParseExtError),
#[error("{0}")]
ParseProgId(#[from] crate::extra::windows::ParseProgIdError),
#[error("{0}")]
ParseProgIdKind(#[from] ParseProgIdKindError),
#[error("{0}")]
BlankPath(#[from] crate::extra::winreg::BlankPathError),
}
/// The result type used in this crate.
pub type Result<T> = std::result::Result<T, Error>;
// endregion
// region: Data Types
/// The token for access registered items in Program.
/// This is usually returned when you registering them.
pub type Token = usize;
// region: Scope
/// The scope where wfassoc will register and unregister application.
#[derive(Debug, Copy, Clone)]
pub enum Scope {
/// Scope for current user.
User,
/// Scope for all users under this computer.
System,
}
/// The error occurs when cast View into Scope.
#[derive(Debug, TeError)]
#[error("hybrid View can not be cast into Scope")]
pub struct TryFromViewError {}
impl TryFromViewError {
fn new() -> Self {
Self {}
}
}
impl TryFrom<View> for Scope {
type Error = TryFromViewError;
fn try_from(value: View) -> std::result::Result<Self, Self::Error> {
match value {
View::User => Ok(Self::User),
View::System => Ok(Self::System),
View::Hybrid => Err(TryFromViewError::new()),
}
}
}
impl Scope {
/// Check whether we have enough privilege when operating in current scope.
/// If we have, return true, otherwise false.
pub fn has_privilege(&self) -> bool {
// If we operate on System, and we do not has privilege,
// we think we do not have privilege, otherwise,
// there is no privilege required.
!matches!(self, Self::System if !utilities::has_privilege())
}
}
// endregion
// region: View
/// The view when wfassoc querying file extension association.
#[derive(Debug, Copy, Clone)]
pub enum View {
/// The view of current user.
User,
/// The view of system.
System,
/// Hybrid view of User and System.
/// It can be seen as that we use System first and then use User to override any existing items.
Hybrid,
}
impl From<Scope> for View {
fn from(value: Scope) -> Self {
match value {
Scope::User => Self::User,
Scope::System => Self::System,
}
}
}
// endregion
// region: ProgId Kind
/// The error occurs when parsing ProgId kind.
#[derive(Debug, TeError)]
pub enum ParseProgIdKindError {
#[error("{0}")]
FromStd(#[from] crate::extra::windows::ParseProgIdError),
#[error("given ProgId is blank")]
BlankProgId,
}
/// The variant of ProgId for the compatibility
/// with those software which do not follow Microsoft suggestions.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ProgIdKind {
/// Other ProgId which not follow Microsoft standards.
Other(String),
/// Standard ProgId.
Std(ProgId),
}
impl Display for ProgIdKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProgIdKind::Other(v) => write!(f, "{}", v),
ProgIdKind::Std(prog_id) => write!(f, "{}", prog_id),
}
}
}
impl FromStr for ProgIdKind {
type Err = ParseProgIdKindError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
if s.is_empty() {
Err(ParseProgIdKindError::BlankProgId)
} else {
Ok(match s.parse::<ProgId>() {
Ok(v) => Self::Std(v),
Err(_) => Self::Other(s.to_string()),
})
}
}
}
// endregion
// endregion
// region: Registry Visitor
// region: Application Visitor
/// The static struct for visiting "Application" in registry
pub struct ApplicationVisitor {}
impl ApplicationVisitor {
const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths";
const APPLICATIONS: &str = "Software\\Classes\\Applications";
/// Open a readonly registry key to "App Paths" with given scope.
pub fn open_app_paths(scope: Scope) -> Result<RegKey> {
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let app_paths = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?;
Ok(app_paths)
}
/// Open a readonly registry key to "Applications" with given scope.
pub fn open_applications(scope: Scope) -> Result<RegKey> {
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let applications = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
Ok(applications)
}
}
// endregion
// region: Classes Visitor
/// The static struct for visiting "Classes" in registry.
pub struct ClassesVisitor {}
impl ClassesVisitor {
const CLASSES: &str = "Software\\Classes";
/// Open a readonly registry key to "Classes" with given view.
pub fn open_with_view(view: View) -> Result<RegKey> {
// Fetch root key and navigate to Classes
let hk = match view {
View::User => RegKey::predef(HKEY_CURRENT_USER),
View::System => RegKey::predef(HKEY_LOCAL_MACHINE),
View::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
};
let classes = match view {
View::User | View::System => hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?,
View::Hybrid => hk.open_subkey_with_flags("", KEY_READ)?,
};
Ok(classes)
}
/// Open a readonly registry key to "Classes" with given scope.
pub fn open_with_scope(scope: Scope) -> Result<RegKey> {
// Fetch root key and navigate to Classes
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let classes = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
Ok(classes)
}
}
// endregion
// endregion
// region: File Extension Registry Key
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ExtKey {
ext: Ext,
}
impl ExtKey {
/// Create new file extension registry key representer.
pub fn new(s: &str) -> Result<Self> {
Ok(Self {
ext: Ext::from_str(s)?,
})
}
/// Fetch the reference to inner extension representer.
pub fn as_inner(&self) -> &Ext {
&self.ext
}
}
impl ExtKey {
/// Set the default "Open With" of this file extension to given ProgId.
pub fn link(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> {
use winreg_extra::blank_path_guard;
// Open Classes key
let classes = ClassesVisitor::open_with_scope(scope)?;
// Open or create this extension key
let (subkey, _) =
classes.create_subkey_with_flags(blank_path_guard(self.ext.to_string())?, KEY_WRITE)?;
// Set the default way to open this file extension
subkey.set_value("", &prog_id.as_inner().to_string())?;
// Okey
Ok(())
}
/// Reset the default "Open With" of this file extension to blank.
///
/// If the default "Open With" of this file extension is not given ProgId,
/// or there is no such file extension, this function do nothing.
pub fn unlink(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> {
use winreg_extra::{blank_path_guard, try_get_value, try_open_subkey_with_flags};
// Open Classes key
let classes = ClassesVisitor::open_with_scope(scope)?;
// Open key for this extension.
// If there is no such key, return directly.
if let Some(subkey) = try_open_subkey_with_flags(
&classes,
blank_path_guard(self.ext.to_string())?,
KEY_WRITE,
)? {
// Only delete the default key if it is equal to our ProgId
if let Some(value) = try_get_value::<String, _>(&subkey, "")? {
if value == prog_id.as_inner().to_string() {
// Delete the default key.
subkey.delete_value("")?;
}
}
}
// Okey
Ok(())
}
/// Query the default "Open With" of this file extension associated ProgId.
///
/// This function will return its associated ProgId if "Open With" was set.
pub fn query(&self, view: View) -> Result<Option<ProgIdKey>> {
use winreg_extra::{blank_path_guard, try_get_value, try_open_subkey_with_flags};
// Open Classes key
let classes = ClassesVisitor::open_with_view(view)?;
// Open key for this extension if possible
let rv = match try_open_subkey_with_flags(
&classes,
blank_path_guard(self.ext.to_string())?,
KEY_READ,
)? {
Some(subkey) => {
// Try get associated ProgId if possible
match try_get_value::<String, _>(&subkey, "")? {
Some(value) => Some(ProgIdKey::new(value.as_str())?),
None => None,
}
}
None => None,
};
// Okey
Ok(rv)
}
}
// endregion
// region: ProgId Registry Key
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ProgIdKey {
prog_id: ProgIdKind,
}
impl ProgIdKey {
/// Create new ProgId registry representer.
pub fn new(s: &str) -> Result<Self> {
Ok(Self {
prog_id: ProgIdKind::from_str(s)?,
})
}
/// Fetch the reference to inner ProgId.
pub fn as_inner(&self) -> &ProgIdKind {
&self.prog_id
}
}
impl ProgIdKey {
/// Create ProgId into Registry in given scope with given parameters
pub fn create(&self, scope: Scope, command: &str) -> Result<()> {
use winreg_extra::blank_path_guard;
let classes = ClassesVisitor::open_with_scope(scope)?;
let (subkey, _) = classes
.create_subkey_with_flags(blank_path_guard(self.prog_id.to_string())?, KEY_WRITE)?;
// Create verb
let (subkey_verb, _) = subkey.create_subkey_with_flags("open", KEY_READ)?;
let (subkey_command, _) = subkey_verb.create_subkey_with_flags("command", KEY_WRITE)?;
subkey_command.set_value("", &command.to_string())?;
Ok(())
}
/// Delete this ProgId from registry in given scope.
pub fn delete(&self, scope: Scope) -> Result<()> {
use winreg_extra::blank_path_guard;
let classes = ClassesVisitor::open_with_scope(scope)?;
classes.delete_subkey_all(blank_path_guard(self.prog_id.to_string())?)?;
Ok(())
}
}
// endregion

View File

@@ -1,61 +1,4 @@
use crate::{ use crate::lowlevel;
lowlevel, utilities,
win32::{self, concept},
};
use regex::Regex;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::path::Path;
use std::sync::Arc;
use std::sync::LazyLock;
use thiserror::Error as TeError;
pub use lowlevel::{Scope, View};
// region: Error Type
/// Error occurs when operating with [Schema].
#[derive(Debug, TeError)]
pub enum SchemaError {
#[error("duplicate key: {0}")]
DuplicateKey(String),
}
/// Error occurs when trying converting [Schema] into [Program].
#[derive(Debug, TeError)]
pub enum ParseProgramError {
#[error("{0}")]
BadExtBody(#[from] concept::BadExtBodyError),
#[error("{0}")]
BadProgIdPart(#[from] concept::BadProgIdPartError),
#[error("{0}")]
BadFileName(#[from] concept::BadFileNameError),
#[error("{0}")]
ParseCmdLine(#[from] concept::ParseCmdLineError),
#[error("{0}")]
CastOsStr(#[from] utilities::CastOsStrError),
#[error("given path doesn't has legal file name part")]
NoFileNamePart,
#[error("given path doesn't has legal directory part")]
NoDirNamePart,
#[error("given identifier is not presented in dict")]
NoSuchIdentifier,
#[error("extension name should not be empty")]
EmptyExtension,
#[error("given program identifier is not allowed")]
BadIdentifier,
}
/// Error occurs when operating with [Program].
#[derive(Debug, TeError)]
pub enum ProgramError {
#[error("{0}")]
Lowlevel(#[from] lowlevel::Error),
#[error("given index is invalid")]
BadIndex,
}
// endregion
// region: Utilities // region: Utilities
@@ -78,697 +21,13 @@ macro_rules! debug_println {
// endregion // endregion
// region: Schema // region: Exposed Stuff
// region: Schema Body mod schema;
mod program;
/// The sketchpad of complete [Program]. pub use schema::{Schema, SchemaError};
/// pub use program::{Program, ParseProgramError, ProgramError, ProgramSelfExtStatus, ProgramExtStatus};
/// In suggested usage, we will create a [Schema] first, pub use lowlevel::{Scope, View};
/// fill some essential and optional properties,
/// then add file extensions which we need.
/// And finally convert it into immutable [Program] for formal using.
#[derive(Debug)]
pub struct Schema {
identifier: String,
path: String,
clsid: String,
name: Option<String>,
icon: Option<String>,
behavior: Option<String>,
strs: HashMap<String, String>,
icons: HashMap<String, String>,
behaviors: HashMap<String, String>,
exts: HashMap<String, SchemaExt>,
}
impl Schema {
pub fn new() -> Self {
Self {
identifier: String::new(),
path: String::new(),
clsid: String::new(),
name: None,
icon: None,
behavior: None,
strs: HashMap::new(),
icons: HashMap::new(),
behaviors: HashMap::new(),
exts: HashMap::new(),
}
}
/// Set the identifier of the schema.
///
/// The identifier is used to build ProgId.
/// So it should starts with an ASCII letter and followed by zero or more ASCII letters, digits, underline and hyphen.
/// And it should not be empty.
pub fn set_identifier(&mut self, identifier: &str) -> () {
self.identifier = identifier.to_string();
}
/// Set the absolute path to the executable file.
pub fn set_path(&mut self, exe_path: &str) -> () {
self.path = exe_path.to_string();
}
pub fn set_clsid(&mut self, clsid: &str) -> () {
self.clsid = clsid.to_string();
}
pub fn set_name(&mut self, name: Option<&str>) -> () {
self.name = name.map(|n| n.to_string());
}
pub fn set_icon(&mut self, icon: Option<&str>) -> () {
self.icon = icon.map(|i| i.to_string());
}
pub fn set_behavior(&mut self, behavior: Option<&str>) -> () {
self.behavior = behavior.map(|b| b.to_string());
}
pub fn add_str(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
match self.strs.insert(name.to_string(), value.to_string()) {
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
None => Ok(()),
}
}
pub fn add_icon(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
match self.icons.insert(name.to_string(), value.to_string()) {
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
None => Ok(()),
}
}
pub fn add_behavior(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
match self.behaviors.insert(name.to_string(), value.to_string()) {
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
None => Ok(()),
}
}
/// Add a file extension to the schema.
///
/// The parameter `ext` is the file extension without leading dot `.`.
pub fn add_ext(
&mut self,
ext: &str,
ext_name: &str,
ext_icon: &str,
ext_behavior: &str,
) -> Result<(), SchemaError> {
match self.exts.insert(
ext.to_string(),
SchemaExt::new(ext_name, ext_icon, ext_behavior),
) {
Some(_) => Err(SchemaError::DuplicateKey(ext.to_string())),
None => Ok(()),
}
}
/// Try converting [Schema] into [Program].
///
/// This is equivalent to [Program::new].
pub fn into_program(self) -> Result<Program, ParseProgramError> {
Program::new(self)
}
}
// endregion
// region: Schema Internals
/// Internal used struct as the Schema file extensions hashmap value type.
#[derive(Debug)]
struct SchemaExt {
name: String,
icon: String,
behavior: String,
}
impl SchemaExt {
fn new(name: &str, icon: &str, behavior: &str) -> Self {
Self {
name: name.to_string(),
icon: icon.to_string(),
behavior: behavior.to_string(),
}
}
}
// endregion
// endregion
// region: Program
// region: Program Body
/// Program is a complete and immutable program representer
pub struct Program {
app_paths_key: lowlevel::AppPathsKey,
applications_key: lowlevel::ApplicationsKey,
app_path: String,
app_dir_path: String,
name: Option<Arc<ProgramStr>>,
icon: Option<Arc<ProgramIcon>>,
behavior: Option<Arc<ProgramBehavior>>,
#[allow(dead_code)]
strs: Vec<Arc<ProgramStr>>,
#[allow(dead_code)]
icons: Vec<Arc<ProgramIcon>>,
#[allow(dead_code)]
behaviors: Vec<Arc<ProgramBehavior>>,
ext_keys: Vec<ProgramProgIdExtKey>,
ext_keys_map: HashMap<String, usize>,
}
impl TryFrom<Schema> for Program {
type Error = ParseProgramError;
fn try_from(value: Schema) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl Program {
/// Extract the file name part from full path to application,
/// which was used in Registry path component.
fn extract_file_name(full_path: &Path) -> Result<&OsStr, ParseProgramError> {
full_path
.file_name()
.ok_or(ParseProgramError::NoFileNamePart)
}
/// Extract the start in path from full path to application,
/// which basically is the stem of full path.
fn extract_dir_path(full_path: &Path) -> Result<&OsStr, ParseProgramError> {
full_path
.parent()
.map(|p| p.as_os_str())
.ok_or(ParseProgramError::NoDirNamePart)
}
fn flat_hashmap<V, U, F>(
hashmap: &HashMap<String, V>,
f: F,
) -> Result<(Vec<U>, HashMap<String, usize>), ParseProgramError>
where
F: Fn(&V) -> Result<U, ParseProgramError>,
{
let mut indexmap: HashMap<String, usize> = HashMap::with_capacity(hashmap.len());
let mut vector: Vec<U> = Vec::with_capacity(hashmap.len());
for (key, value) in hashmap.into_iter() {
indexmap.insert(key.clone(), vector.len());
vector.push(f(value)?);
}
Ok((vector, indexmap))
}
fn resolve_index<T>(
key: &String,
vector: &Vec<Arc<T>>,
index_map: &HashMap<String, usize>,
) -> Result<Arc<T>, ParseProgramError> {
match index_map.get(key) {
Some(index) => Ok(vector
.get(*index)
.expect("unexpected invalid index")
.clone()),
None => Err(ParseProgramError::NoSuchIdentifier),
}
}
/// Build ProgId from identifier and given file extension.
fn build_progid(
identifier: &str,
ext: &str,
) -> Result<lowlevel::LosseProgId, ParseProgramError> {
// Use Regex to check identifier
static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^[a-zA-Z][a-zA-Z0-9_-]*$").expect("unexpected bad regex pattern string")
});
let identifier = match RE.captures(identifier) {
Some(_) => identifier,
None => return Err(ParseProgramError::BadIdentifier),
};
// Capitalize first ASCII of ext
let ext = utilities::capitalize_first_ascii(ext);
// Build strict ProgId
let progid = concept::ProgId::new(identifier, &ext, None)?;
// Then build losse ProgId
let losse_progid: lowlevel::LosseProgId = progid.into();
// Return built result
Ok(losse_progid)
}
/// Try converting [Schema] into [Program].
///
/// During this process, some checks will be performed to ensure the validity of the data.
/// For example, the reference to icon, name, or behavior must exist in their respective dictionaries.
/// The identifier must be suit for building ProgId.
pub fn new(schema: Schema) -> Result<Self, ParseProgramError> {
// Extract file name part and directory name part respectively.
let schema_path = Path::new(&schema.path);
let app_path = schema.path.clone();
let app_file_name = Self::extract_file_name(schema_path)?;
let app_file_name = String::from(utilities::osstr_to_str(app_file_name)?);
let app_dir_path = Self::extract_dir_path(schema_path)?;
let app_dir_path = String::from(utilities::osstr_to_str(app_dir_path)?);
// Build app paths key and applications key respectively
let key = concept::FileName::new(&app_file_name)?;
let app_paths_key = lowlevel::AppPathsKey::new(key.clone());
let applications_key = lowlevel::ApplicationsKey::new(key.clone());
// Build string, icon and behavior list,
// and build mapper at the same time.
let (strs, strs_index_map) = Self::flat_hashmap(&schema.strs, |entry| {
let str_res_variant: lowlevel::StrResVariant = entry.as_str().into();
let program_str = ProgramStr {
inner: str_res_variant,
};
Ok(Arc::new(program_str))
})?;
let (icons, icons_index_map) = Self::flat_hashmap(&schema.icons, |entry| {
let icon_res_variant: lowlevel::IconResVariant = entry.as_str().into();
let program_icon = ProgramIcon {
inner: icon_res_variant,
};
Ok(Arc::new(program_icon))
})?;
let (behaviors, behaviors_index_map) = Self::flat_hashmap(&schema.behaviors, |entry| {
// We simply always use "Open" verb.
let cmdline: concept::CmdLine = entry.as_str().parse()?;
let verb = concept::Verb::OPEN();
let shell_verb = lowlevel::ShellVerb::new(verb, cmdline);
let program_behavior = ProgramBehavior { inner: shell_verb };
Ok(Arc::new(program_behavior))
})?;
// Setup default name, icon and behavior
let name = schema
.name
.map(|name| Self::resolve_index(&name, &strs, &strs_index_map))
.transpose()?;
let icon = schema
.icon
.map(|icon| Self::resolve_index(&icon, &icons, &icons_index_map))
.transpose()?;
let behavior = schema
.behavior
.map(|behavior| Self::resolve_index(&behavior, &behaviors, &behaviors_index_map))
.transpose()?;
// We build ext keys
let mut ext_keys: Vec<ProgramProgIdExtKey> = Vec::with_capacity(schema.exts.len());
for (key, value) in &schema.exts {
// Build ProgId first.
let progid = Self::build_progid(&schema.identifier, key.as_str())?;
// Then build ProgId key.
let progid_key = lowlevel::ProgIdKey::new(progid);
// Build essential fields for program ProgId key struct.
let name = Self::resolve_index(&value.name, &strs, &strs_index_map)?;
let icon = Self::resolve_index(&value.icon, &icons, &icons_index_map)?;
let behavior = Self::resolve_index(&value.behavior, &behaviors, &behaviors_index_map)?;
// Build Ext.
let ext = concept::Ext::new(key.as_str())?;
let ext_key = lowlevel::ExtKey::new(ext);
// Create program ProgId Ext key struct
let progid_ext_key = ProgramProgIdExtKey {
ext_key,
progid_key,
name,
icon,
behavior,
};
// Add them into list
ext_keys.push(progid_ext_key);
}
// The build ext keys map
let ext_keys_map = ext_keys
.iter()
.enumerate()
.map(|(i, ext)| (ext.ext_key.inner().inner().to_string(), i))
.collect();
// Everything is okey
Ok(Self {
app_paths_key,
applications_key,
app_path,
app_dir_path,
name,
icon,
behavior,
strs,
icons,
behaviors,
ext_keys,
ext_keys_map,
})
}
}
impl Program {
pub fn resolve_name(&self) -> Result<String, ProgramError> {
todo!()
}
pub fn resolve_icon(&self) -> Result<concept::IconRc, ProgramError> {
todo!()
}
pub fn get_ext_count(&self) -> usize {
self.ext_keys.len()
}
pub fn get_ext(&self, index: usize) -> Result<&concept::Ext, ProgramError> {
match self.ext_keys.get(index) {
Some(program_key) => {
let ext_key = &program_key.ext_key;
Ok(ext_key.inner())
}
None => Err(ProgramError::BadIndex),
}
}
pub fn find_ext(&self, body: &str) -> Option<usize> {
self.ext_keys_map.get(body).copied()
}
}
impl Program {
/// Register this application.
///
/// If there is complete or partial registration of this application
/// (partial registration may occurs when registration failed),
/// this function does nothing.
pub fn register(&mut self, scope: Scope) -> Result<(), ProgramError> {
// Create App Paths subkey
debug_println!("Adding App Paths subkey...");
self.app_paths_key.ensure(scope)?;
// Write App Paths values
self.app_paths_key.set_default(scope, &self.app_path)?;
self.app_paths_key.set_path(scope, &self.app_dir_path)?;
// Create Applications subkey
debug_println!("Adding Applications subkey...");
self.applications_key.ensure(scope)?;
// Write Applications values
self.applications_key
.set_shell_verb(scope, self.behavior.as_ref().map(|beh| &beh.inner))?;
self.applications_key
.set_default_icon(scope, self.icon.as_ref().map(|ico| &ico.inner))?;
self.applications_key
.set_friendly_app_name(scope, self.name.as_ref().map(|name| &name.inner))?;
let exts: Vec<&concept::Ext> = self
.ext_keys
.iter()
.map(|key| key.ext_key.inner())
.collect();
self.applications_key
.set_supported_types(scope, Some(&exts))?;
self.applications_key.set_no_open_with(scope, true)?;
// Create ProgId subkeys one by one
debug_println!("Adding ProgId subkey...");
for program_key in &mut self.ext_keys {
let progid_key = &mut program_key.progid_key;
debug_println!(
"Adding ProgId \"{0}\" subkey...",
progid_key.inner().to_string()
);
// Create ProgId subkey
progid_key.ensure(scope)?;
// Write ProgId values
let name = Some(&program_key.name.inner);
progid_key.set_default(scope, name)?;
progid_key.set_shell_verb(scope, &program_key.behavior.inner)?;
progid_key.set_friendly_type_name(scope, name)?;
progid_key.set_default_icon(scope, Some(&program_key.icon.inner))?;
// Add this progid to file extension "open with" list.
let ext_key = &mut program_key.ext_key;
ext_key.add_into_open_with_progids(scope, progid_key.inner())?;
}
// Everything is okey.
// Notify changes and return
win32::utilities::notify_assoc_changed();
Ok(())
}
/// Unregister this application.
///
/// No matter whether there is registration of this application,
/// this function always make sure that there is no registration after running this function.
pub fn unregister(&mut self, scope: Scope) -> Result<(), ProgramError> {
// Delete App Paths subkey
debug_println!("Deleting App Paths subkey...");
self.app_paths_key.delete(scope)?;
// Delete Applications subkey
debug_println!("Deleting Applications subkey...");
self.applications_key.delete(scope)?;
// Delete ProgId subkeys one by one.
debug_println!("Adding ProgId subkey...");
for program_key in &mut self.ext_keys {
let progid_key = &mut program_key.progid_key;
debug_println!(
"Deleting ProgId \"{0}\" subkey...",
progid_key.inner().to_string()
);
// YYC MARK:
// According to Microsoft document, when uninstalling application,
// there is no need to reset the default open way of file extension.
// So we simply remove it from "open with" list.
// Remove this ProgId from file extension "open with" list.
let ext_key = &mut program_key.ext_key;
ext_key.remove_from_open_with_progids(scope, progid_key.inner())?;
// Delete ProgId subkey
progid_key.delete(scope)?;
}
// Everything is okey.
// Notify changes and return
win32::utilities::notify_assoc_changed();
Ok(())
}
/// Check whether this application has been registered in given view.
///
/// Please note that this is a rough check and do not validate any data.
///
/// The return value only ensures the pre-requirement of `register` and `unregister`.
pub fn is_registered(&self, scope: Scope) -> Result<bool, ProgramError> {
// Check App Paths subkey.
debug_println!("Checking App Paths subkey...");
if !self.app_paths_key.is_exist(scope)? {
return Ok(false);
}
// Check Application subkey.
debug_println!("Checking Applications subkey...");
if !self.applications_key.is_exist(scope.into())? {
return Ok(false);
}
// Check ProgId subkey.
debug_println!("Checking ProgId subkey...");
for program_key in &self.ext_keys {
let progid_key = &program_key.progid_key;
debug_println!(
"Checking ProgId \"{0}\" subkey...",
progid_key.inner().to_string()
);
if !progid_key.is_exist(scope.into())? {
return Ok(false);
}
}
// Every subkeys are roughly existing.
Ok(true)
}
pub fn link_ext(&mut self, scope: Scope, index: usize) -> Result<(), ProgramError> {
match self.ext_keys.get_mut(index) {
Some(program_key) => {
let ext_key = &mut program_key.ext_key;
let progid_key = &program_key.progid_key;
debug_println!(
"Linking ProgId \"{0}\" to extension \"{1}\"subkey...",
progid_key.inner().to_string(),
ext_key.inner().to_string()
);
// Before setting it, we must make sure this extension is existing
ext_key.ensure(scope)?;
ext_key.set_default(scope, Some(progid_key.inner()))?;
Ok(())
}
None => Err(ProgramError::BadIndex),
}
}
pub fn unlink_ext(&mut self, scope: Scope, index: usize) -> Result<(), ProgramError> {
match self.ext_keys.get_mut(index) {
Some(program_key) => {
let ext_key = &mut program_key.ext_key;
debug_println!(
"Unlinking for extension \"{0}\"subkey...",
ext_key.inner().to_string()
);
// Before setting it, we must make sure this extension is existing
ext_key.ensure(scope)?;
ext_key.set_default(scope, None)?;
Ok(())
}
None => Err(ProgramError::BadIndex),
}
}
pub fn query_ext(
&self,
view: View,
index: usize,
) -> Result<Option<ProgramExtStatus>, ProgramError> {
match self.ext_keys.get(index) {
Some(program_key) => {
let ext_key = &program_key.ext_key;
debug_println!(
"Querying for extension \"{0}\"subkey...",
ext_key.inner().to_string()
);
// If there is no such extension key, return None about this extension.
if !ext_key.is_exist(view)? {
return Ok(None);
}
// Let we fetch its associated default ProgId.
// If there is no such key, return None instead.
let progid = match ext_key.get_default(view)? {
Some(progid) => progid,
None => return Ok(None),
};
// Now we build ProgId key from gotten association
let progid_key = lowlevel::ProgIdKey::new(progid);
// If this associated ProgId key is not presented,
// we return None instead.
if !progid_key.is_exist(view)? {
return Ok(None);
}
// Now try fetch its diaplay name in modern way first.
// If there is no modern way, use legacy way instead.
// If there is still no display name, use ProgId self instead as display name.
let name = match progid_key.get_friendly_type_name(view)? {
Some(name) => name.extract()?,
None => match progid_key.get_default(view)? {
Some(name) => name.extract()?,
None => progid_key.inner().to_string(),
},
};
// Now try to fetch icon.
let icon = progid_key
.get_default_icon(view)?
.map(|ico| ico.extract(concept::IconSizeKind::Small))
.transpose()?;
// Okey, return it.
Ok(Some(ProgramExtStatus::new(name, icon)))
}
None => Err(ProgramError::BadIndex),
}
}
}
// endregion
// region: Program Exposed Structs
/// Exposed struct representing the default associated program of specific file extension.
///
/// The data including the diaplay name and icon.
pub struct ProgramExtStatus {
name: String,
icon: Option<concept::IconRc>,
}
impl ProgramExtStatus {
fn new(name: String, icon: Option<concept::IconRc>) -> Self {
Self { name, icon }
}
/// Get the display name of this program.
///
/// The program provided display name will be used firstly.
/// If this program has no display name, the stringified ProgId will be used instead.
pub fn get_name(&self) -> &str {
self.name.as_str()
}
/// Get the icon of this program.
///
/// Due to the icon is optional, if there is no icon, return None.
pub fn get_icon(&self) -> Option<&concept::IconRc> {
self.icon.as_ref()
}
}
// endregion
// region: Program Internals
/// Internal used enum presenting a Program string resource.
#[derive(Debug)]
struct ProgramStr {
inner: lowlevel::StrResVariant,
}
/// Internal used enum presenting a Program icon resource.
#[derive(Debug)]
struct ProgramIcon {
inner: lowlevel::IconResVariant,
}
/// Internal used enum presenting a Program behavior (command line setups).
#[derive(Debug)]
struct ProgramBehavior {
inner: lowlevel::ShellVerb,
}
/// Internal used struct presenting a Program ProgId and associated file extension.
///
/// Another reason combine ProgId and Ext is that we can't operate ProgId in mutable mode,
/// due to the internal immutable of Rc in Rust.
#[derive(Debug)]
struct ProgramProgIdExtKey {
ext_key: lowlevel::ExtKey,
progid_key: lowlevel::ProgIdKey,
name: Arc<ProgramStr>,
icon: Arc<ProgramIcon>,
behavior: Arc<ProgramBehavior>,
}
// endregion
// endregion // endregion

View File

@@ -0,0 +1,716 @@
use super::Schema;
use crate::{
lowlevel::{self, Scope, View},
utilities,
win32::{self, concept},
};
use regex::Regex;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::path::Path;
use std::sync::Arc;
use std::sync::LazyLock;
use thiserror::Error as TeError;
// region: Error Type
/// Error occurs when trying converting [Schema] into [Program].
#[derive(Debug, TeError)]
pub enum ParseProgramError {
#[error("{0}")]
BadExtBody(#[from] concept::BadExtBodyError),
#[error("{0}")]
BadProgIdPart(#[from] concept::BadProgIdPartError),
#[error("{0}")]
BadFileName(#[from] concept::BadFileNameError),
#[error("{0}")]
ParseCmdLine(#[from] concept::ParseCmdLineError),
#[error("{0}")]
CastOsStr(#[from] utilities::CastOsStrError),
#[error("given path doesn't has legal file name part")]
NoFileNamePart,
#[error("given path doesn't has legal directory part")]
NoDirNamePart,
#[error("given identifier is not presented in dict")]
NoSuchIdentifier,
#[error("extension name should not be empty")]
EmptyExtension,
#[error("given program identifier is not allowed")]
BadIdentifier,
}
/// Error occurs when operating with [Program].
#[derive(Debug, TeError)]
pub enum ProgramError {
#[error("{0}")]
Lowlevel(#[from] lowlevel::Error),
#[error("{0}")]
LoadIconRc(#[from] concept::LoadIconRcError),
#[error("given index is invalid")]
BadIndex,
}
// endregion
// region: Program
/// Program is a complete and immutable program representer
#[derive(Debug)]
pub struct Program {
app_paths_key: lowlevel::AppPathsKey,
applications_key: lowlevel::ApplicationsKey,
app_path: String,
app_file_name: String,
app_dir_path: String,
name: Option<Arc<ProgramStr>>,
icon: Option<Arc<ProgramIcon>>,
behavior: Option<Arc<ProgramBehavior>>,
#[allow(dead_code)]
strs: Vec<Arc<ProgramStr>>,
#[allow(dead_code)]
icons: Vec<Arc<ProgramIcon>>,
#[allow(dead_code)]
behaviors: Vec<Arc<ProgramBehavior>>,
ext_keys: Vec<ProgramProgIdExtKey>,
ext_keys_map: HashMap<String, usize>,
}
impl TryFrom<Schema> for Program {
type Error = ParseProgramError;
fn try_from(value: Schema) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl Program {
/// Extract the file name part from full path to application,
/// which was used in Registry path component.
fn extract_file_name(full_path: &Path) -> Result<&OsStr, ParseProgramError> {
full_path
.file_name()
.ok_or(ParseProgramError::NoFileNamePart)
}
/// Extract the start in path from full path to application,
/// which basically is the stem of full path.
fn extract_dir_path(full_path: &Path) -> Result<&OsStr, ParseProgramError> {
full_path
.parent()
.map(|p| p.as_os_str())
.ok_or(ParseProgramError::NoDirNamePart)
}
fn flat_hashmap<V, U, F>(
hashmap: &HashMap<String, V>,
f: F,
) -> Result<(Vec<U>, HashMap<String, usize>), ParseProgramError>
where
F: Fn(&V) -> Result<U, ParseProgramError>,
{
let mut indexmap: HashMap<String, usize> = HashMap::with_capacity(hashmap.len());
let mut vector: Vec<U> = Vec::with_capacity(hashmap.len());
for (key, value) in hashmap.into_iter() {
indexmap.insert(key.clone(), vector.len());
vector.push(f(value)?);
}
Ok((vector, indexmap))
}
fn resolve_index<T>(
key: &str,
vector: &Vec<Arc<T>>,
index_map: &HashMap<String, usize>,
) -> Result<Arc<T>, ParseProgramError> {
match index_map.get(key) {
Some(index) => Ok(vector
.get(*index)
.expect("unexpected invalid index")
.clone()),
None => Err(ParseProgramError::NoSuchIdentifier),
}
}
/// Build ProgId from identifier and given file extension.
fn build_progid(
identifier: &str,
ext: &str,
) -> Result<lowlevel::LosseProgId, ParseProgramError> {
// Use Regex to check identifier
static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^[a-zA-Z][a-zA-Z0-9_-]*$").expect("unexpected bad regex pattern string")
});
let identifier = match RE.captures(identifier) {
Some(_) => identifier,
None => return Err(ParseProgramError::BadIdentifier),
};
// Capitalize first ASCII of ext
let ext = utilities::capitalize_first_ascii(ext);
// Build strict ProgId
let progid = concept::ProgId::new(identifier, &ext, None)?;
// Then build losse ProgId
let losse_progid: lowlevel::LosseProgId = progid.into();
// Return built result
Ok(losse_progid)
}
/// Try converting [Schema] into [Program].
///
/// During this process, some checks will be performed to ensure the validity of the data.
/// For example, the reference to icon, name, or behavior must exist in their respective dictionaries.
/// The identifier must be suit for building ProgId.
pub fn new(schema: Schema) -> Result<Self, ParseProgramError> {
// Extract file name part and directory name part respectively.
let schema_path = Path::new(schema.get_path());
let app_path = schema.get_path().to_string();
let app_file_name = Self::extract_file_name(schema_path)?;
let app_file_name = String::from(utilities::osstr_to_str(app_file_name)?);
let app_dir_path = Self::extract_dir_path(schema_path)?;
let app_dir_path = String::from(utilities::osstr_to_str(app_dir_path)?);
// Build app paths key and applications key respectively
let key = concept::FileName::new(&app_file_name)?;
let app_paths_key = lowlevel::AppPathsKey::new(key.clone());
let applications_key = lowlevel::ApplicationsKey::new(key.clone());
// Build string, icon and behavior list,
// and build mapper at the same time.
let (strs, strs_index_map) = Self::flat_hashmap(schema.get_strs(), |entry| {
let str_res_variant: lowlevel::StrResVariant = entry.as_str().into();
let program_str = ProgramStr {
inner: str_res_variant,
};
Ok(Arc::new(program_str))
})?;
let (icons, icons_index_map) = Self::flat_hashmap(schema.get_icons(), |entry| {
let icon_res_variant: lowlevel::IconResVariant = entry.as_str().into();
let program_icon = ProgramIcon {
inner: icon_res_variant,
};
Ok(Arc::new(program_icon))
})?;
let (behaviors, behaviors_index_map) =
Self::flat_hashmap(schema.get_behaviors(), |entry| {
// We simply always use "Open" verb.
let cmdline: concept::CmdLine = entry.as_str().parse()?;
let verb = concept::Verb::OPEN();
let shell_verb = lowlevel::ShellVerb::new(verb, cmdline);
let program_behavior = ProgramBehavior { inner: shell_verb };
Ok(Arc::new(program_behavior))
})?;
// Setup default name, icon and behavior
let name = schema
.get_name()
.map(|name| Self::resolve_index(name, &strs, &strs_index_map))
.transpose()?;
let icon = schema
.get_icon()
.map(|icon| Self::resolve_index(icon, &icons, &icons_index_map))
.transpose()?;
let behavior = schema
.get_behavior()
.map(|behavior| Self::resolve_index(behavior, &behaviors, &behaviors_index_map))
.transpose()?;
// We build ext keys
let mut ext_keys: Vec<ProgramProgIdExtKey> = Vec::with_capacity(schema.get_exts().len());
for (key, value) in schema.get_exts() {
// Build ProgId first.
let progid = Self::build_progid(schema.get_identifier(), key.as_str())?;
// Then build ProgId key.
let progid_key = lowlevel::ProgIdKey::new(progid);
// Build essential fields for program ProgId key struct.
let name = Self::resolve_index(value.get_name(), &strs, &strs_index_map)?;
let icon = Self::resolve_index(value.get_icon(), &icons, &icons_index_map)?;
let behavior =
Self::resolve_index(value.get_behavior(), &behaviors, &behaviors_index_map)?;
// Build Ext.
let ext = concept::Ext::new(key.as_str())?;
let ext_key = lowlevel::ExtKey::new(ext);
// Create program ProgId Ext key struct
let progid_ext_key = ProgramProgIdExtKey {
ext_key,
progid_key,
name,
icon,
behavior,
};
// Add them into list
ext_keys.push(progid_ext_key);
}
// The build ext keys map
let ext_keys_map = ext_keys
.iter()
.enumerate()
.map(|(i, ext)| (ext.ext_key.inner().inner().to_string(), i))
.collect();
// Everything is okey
Ok(Self {
app_paths_key,
applications_key,
app_path,
app_file_name,
app_dir_path,
name,
icon,
behavior,
strs,
icons,
behaviors,
ext_keys,
ext_keys_map,
})
}
}
impl Program {
pub fn resolve_name(&self) -> Result<String, ProgramError> {
// Fecch from user specified name first
let name = self
.name
.as_ref()
.map(|name| name.inner.extract().ok())
.flatten();
if let Some(name) = name {
return Ok(name);
}
// TODO:
// Fetch it from executable manifest file.
// Finally fallback to use executable name.
Ok(self.app_file_name.clone())
}
pub fn resolve_icon(&self) -> Result<concept::IconRc, ProgramError> {
// Fetch from user specified icon first
let icon = self
.icon
.as_ref()
.map(|icon| icon.inner.extract(concept::IconSizeKind::Small).ok())
.flatten();
if let Some(icon) = icon {
return Ok(icon);
}
// Fetch from the first icon of executable instead.
let icon = concept::IconRc::new(&self.app_path, 0, concept::IconSizeKind::Small).ok();
if let Some(icon) = icon {
return Ok(icon);
}
// Finally fallback to use system default executable icon.
Ok(concept::IconRc::GENERIC_APPLICATION(
concept::IconSizeKind::Small,
)?)
}
pub fn exts_len(&self) -> usize {
self.ext_keys.len()
}
pub fn find_ext(&self, body: &str) -> Option<usize> {
self.ext_keys_map.get(body).copied()
}
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)?;
// Try resolving name with string resource first,
// and fallback to ProgId verbatim.
let name = progid_ext_key
.name
.inner
.extract()
.unwrap_or(progid_ext_key.progid_key.inner().to_string());
// Try to fetch icon, and fallback to system default file icon.
let icon = progid_ext_key
.icon
.inner
.extract(concept::IconSizeKind::Small)
.unwrap_or(concept::IconRc::GENERIC_DOCUMENT(
concept::IconSizeKind::Small,
)?);
// Okey, return it
Ok(ProgramSelfExtStatus::new(
progid_ext_key.ext_key.inner().clone(),
name,
icon,
))
}
}
impl Program {
/// Register this application.
///
/// If there is complete or partial registration of this application
/// (partial registration may occurs when registration failed),
/// this function does nothing.
pub fn register(&mut self, scope: Scope) -> Result<(), ProgramError> {
// Create App Paths subkey
debug_println!("Adding App Paths subkey...");
self.app_paths_key.ensure(scope)?;
// Write App Paths values
self.app_paths_key.set_default(scope, &self.app_path)?;
self.app_paths_key.set_path(scope, &self.app_dir_path)?;
// Create Applications subkey
debug_println!("Adding Applications subkey...");
self.applications_key.ensure(scope)?;
// Write Applications values
self.applications_key
.set_shell_verb(scope, self.behavior.as_ref().map(|beh| &beh.inner))?;
self.applications_key
.set_default_icon(scope, self.icon.as_ref().map(|ico| &ico.inner))?;
self.applications_key
.set_friendly_app_name(scope, self.name.as_ref().map(|name| &name.inner))?;
let exts: Vec<&concept::Ext> = self
.ext_keys
.iter()
.map(|key| key.ext_key.inner())
.collect();
self.applications_key
.set_supported_types(scope, Some(&exts))?;
self.applications_key.set_no_open_with(scope, true)?;
// Create ProgId subkeys one by one
debug_println!("Adding ProgId subkey...");
for program_key in &mut self.ext_keys {
let progid_key = &mut program_key.progid_key;
debug_println!(
"Adding ProgId \"{0}\" subkey...",
progid_key.inner().to_string()
);
// Create ProgId subkey
progid_key.ensure(scope)?;
// Write ProgId values
let name = Some(&program_key.name.inner);
progid_key.set_default(scope, name)?;
progid_key.set_shell_verb(scope, Some(&program_key.behavior.inner))?;
progid_key.set_friendly_type_name(scope, name)?;
progid_key.set_default_icon(scope, Some(&program_key.icon.inner))?;
// Add this progid to file extension "open with" list.
let ext_key = &mut program_key.ext_key;
ext_key.ensure(scope)?;
ext_key.add_into_open_with_progids(scope, progid_key.inner())?;
}
// Everything is okey.
// Notify changes and return
win32::utilities::notify_assoc_changed();
Ok(())
}
/// Unregister this application.
///
/// No matter whether there is registration of this application,
/// this function always make sure that there is no registration after running this function.
pub fn unregister(&mut self, scope: Scope) -> Result<(), ProgramError> {
// Delete App Paths subkey
debug_println!("Deleting App Paths subkey...");
self.app_paths_key.delete(scope)?;
// Delete Applications subkey
debug_println!("Deleting Applications subkey...");
self.applications_key.delete(scope)?;
// Delete ProgId subkeys one by one.
debug_println!("Adding ProgId subkey...");
for program_key in &mut self.ext_keys {
let progid_key = &mut program_key.progid_key;
debug_println!(
"Deleting ProgId \"{0}\" subkey...",
progid_key.inner().to_string()
);
// YYC MARK:
// According to Microsoft document, when uninstalling application,
// there is no need to reset the default open way of file extension.
// So we simply remove it from "open with" list.
// Remove this ProgId from file extension "open with" list,
// if this file extension is existing
let ext_key = &mut program_key.ext_key;
if ext_key.is_exist(scope.into())? {
ext_key.remove_from_open_with_progids(scope, progid_key.inner())?;
}
// Delete ProgId subkey
progid_key.delete(scope)?;
}
// Everything is okey.
// Notify changes and return
win32::utilities::notify_assoc_changed();
Ok(())
}
/// Check whether this application has been registered in given view.
///
/// Please note that this is a rough check and do not validate any data.
///
/// The return value only ensures the pre-requirement of `register` and `unregister`.
pub fn is_registered(&self, scope: Scope) -> Result<bool, ProgramError> {
// Check App Paths subkey.
debug_println!("Checking App Paths subkey...");
if !self.app_paths_key.is_exist(scope)? {
return Ok(false);
}
// Check Application subkey.
debug_println!("Checking Applications subkey...");
if !self.applications_key.is_exist(scope.into())? {
return Ok(false);
}
// Check ProgId subkey.
debug_println!("Checking ProgId subkey...");
for program_key in &self.ext_keys {
let progid_key = &program_key.progid_key;
debug_println!(
"Checking ProgId \"{0}\" subkey...",
progid_key.inner().to_string()
);
if !progid_key.is_exist(scope.into())? {
return Ok(false);
}
}
// Every subkeys are roughly existing.
Ok(true)
}
pub fn link_ext(&mut self, scope: Scope, index: usize) -> Result<(), ProgramError> {
match self.ext_keys.get_mut(index) {
Some(program_key) => {
let ext_key = &mut program_key.ext_key;
let progid_key = &program_key.progid_key;
debug_println!(
"Linking ProgId \"{0}\" to extension \"{1}\" subkey...",
progid_key.inner().to_string(),
ext_key.inner().to_string()
);
// Before setting it, we must make sure this extension is existing
ext_key.ensure(scope)?;
ext_key.set_default(scope, Some(progid_key.inner()))?;
}
None => return Err(ProgramError::BadIndex),
};
// Everything is okey.
// Notify changes and return
win32::utilities::notify_assoc_changed();
Ok(())
}
pub fn unlink_ext(&mut self, scope: Scope, index: usize) -> Result<(), ProgramError> {
match self.ext_keys.get_mut(index) {
Some(program_key) => {
let ext_key = &mut program_key.ext_key;
debug_println!(
"Unlinking for extension \"{0}\" subkey...",
ext_key.inner().to_string()
);
// Before setting it, we must make sure this extension is existing
ext_key.ensure(scope)?;
ext_key.set_default(scope, None)?;
}
None => return Err(ProgramError::BadIndex),
}
// Everything is okey.
// Notify changes and return
win32::utilities::notify_assoc_changed();
Ok(())
}
pub fn query_ext(
&self,
view: View,
index: usize,
) -> Result<Option<ProgramExtStatus>, ProgramError> {
match self.ext_keys.get(index) {
Some(program_key) => {
let ext_key = &program_key.ext_key;
debug_println!(
"Querying for extension \"{0}\"subkey...",
ext_key.inner().to_string()
);
// If there is no such extension key, return None about this extension.
if !ext_key.is_exist(view)? {
return Ok(None);
}
// Let we fetch its associated default ProgId.
// If there is no such key, return None instead.
let progid = match ext_key.get_default(view)? {
Some(progid) => progid,
None => return Ok(None),
};
// Now we build ProgId key from gotten association
let progid_key = lowlevel::ProgIdKey::new(progid);
// If this associated ProgId key is not presented,
// we return None instead.
if !progid_key.is_exist(view)? {
return Ok(None);
}
// Now try fetch its diaplay name in modern way first.
// If there is no modern way, use legacy way instead.
// If there is still no display name, use ProgId self instead as display name.
let mut name: Option<String> = None;
if let None = name {
name = progid_key
.get_friendly_type_name(view)?
.map(|name| name.extract().ok())
.flatten();
}
if let None = name {
name = progid_key
.get_default(view)?
.map(|name| name.extract().ok())
.flatten();
}
let name = name.unwrap_or(progid_key.inner().to_string());
// Now try to fetch icon and fallback to system default file icon.
let icon = progid_key
.get_default_icon(view)?
.map(|ico| ico.extract(concept::IconSizeKind::Small).ok())
.flatten()
.unwrap_or(concept::IconRc::GENERIC_DOCUMENT(
concept::IconSizeKind::Small,
)?);
// Okey, return it.
Ok(Some(ProgramExtStatus::new(name, icon)))
}
None => Err(ProgramError::BadIndex),
}
}
}
// endregion
// region: Internal Stuff
/// Internal used enum presenting a Program string resource.
#[derive(Debug)]
struct ProgramStr {
inner: lowlevel::StrResVariant,
}
/// Internal used enum presenting a Program icon resource.
#[derive(Debug)]
struct ProgramIcon {
inner: lowlevel::IconResVariant,
}
/// Internal used enum presenting a Program behavior (command line setups).
#[derive(Debug)]
struct ProgramBehavior {
inner: lowlevel::ShellVerb,
}
/// Internal used struct presenting a Program ProgId and associated file extension.
///
/// Another reason combine ProgId and Ext is that we can't operate ProgId in mutable mode,
/// due to the internal immutable of Rc in Rust.
#[derive(Debug)]
struct ProgramProgIdExtKey {
ext_key: lowlevel::ExtKey,
progid_key: lowlevel::ProgIdKey,
name: Arc<ProgramStr>,
icon: Arc<ProgramIcon>,
behavior: Arc<ProgramBehavior>,
}
// endregion
// region: Exposed Stuff
/// Exposed struct representing this program provided method for opening specific file extension.
///
/// The data including the extension name, diaplay name and icon.
#[derive(Debug)]
pub struct ProgramSelfExtStatus {
ext: concept::Ext,
name: String,
icon: concept::IconRc,
}
impl ProgramSelfExtStatus {
fn new(ext: concept::Ext, name: String, icon: concept::IconRc) -> Self {
Self { ext, name, icon }
}
/// Get the extension name without leading dot like `jpg`.
pub fn get_ext(&self) -> &str {
self.ext.inner()
}
/// Get the extension name with leading dot like `.jpg`.
pub fn get_dotted_ext(&self) -> String {
self.ext.dotted_inner()
}
/// Get the display name of this program.
///
/// The program provided display name will be used firstly.
/// If this program has no display name, the stringified ProgId will be used instead.
pub fn get_name(&self) -> &str {
self.name.as_str()
}
/// Get the icon of this program.
///
/// Due to the icon is optional, if there is no icon, return None.
pub fn get_icon(&self) -> &concept::IconRc {
&self.icon
}
}
/// Exposed struct representing the default associated program of specific file extension.
///
/// The data including the diaplay name and icon.
#[derive(Debug)]
pub struct ProgramExtStatus {
name: String,
icon: concept::IconRc,
}
impl ProgramExtStatus {
fn new(name: String, icon: concept::IconRc) -> Self {
Self { name, icon }
}
/// Get the display name of this program.
///
/// The program provided display name will be used firstly.
/// If this program has no display name, the stringified ProgId will be used instead.
pub fn get_name(&self) -> &str {
self.name.as_str()
}
/// Get the icon of this program.
///
/// Due to the icon is optional, if there is no icon, return None.
pub fn get_icon(&self) -> &concept::IconRc {
&self.icon
}
}
// endregion

View File

@@ -0,0 +1,215 @@
use std::collections::HashMap;
use thiserror::Error as TeError;
use super::{Program, ParseProgramError};
// region: Error Type
/// Error occurs when operating with [Schema].
#[derive(Debug, TeError)]
pub enum SchemaError {
#[error("duplicate key: {0}")]
DuplicateKey(String),
}
// endregion
// region: Schema
/// The sketchpad of complete [Program].
///
/// In suggested usage, we will create a [Schema] first,
/// fill some essential and optional properties,
/// then add file extensions which we need.
/// And finally convert it into immutable [Program] for formal using.
#[derive(Debug)]
pub struct Schema {
identifier: String,
path: String,
clsid: String,
name: Option<String>,
icon: Option<String>,
behavior: Option<String>,
strs: HashMap<String, String>,
icons: HashMap<String, String>,
behaviors: HashMap<String, String>,
exts: HashMap<String, SchemaExt>,
}
impl Schema {
pub fn new() -> Self {
Self {
identifier: String::new(),
path: String::new(),
clsid: String::new(),
name: None,
icon: None,
behavior: None,
strs: HashMap::new(),
icons: HashMap::new(),
behaviors: HashMap::new(),
exts: HashMap::new(),
}
}
/// Try converting [Schema] into [Program].
///
/// This is equivalent to [Program::new].
pub fn into_program(self) -> Result<Program, ParseProgramError> {
Program::new(self)
}
}
impl Schema {
/// Set the identifier of the schema.
///
/// The identifier is used to build ProgId.
/// So it should starts with an ASCII letter and followed by zero or more ASCII letters, digits, underline and hyphen.
/// And it should not be empty.
pub fn set_identifier(&mut self, identifier: &str) -> () {
self.identifier = identifier.to_string();
}
/// Set the absolute path to the executable file.
pub fn set_path(&mut self, exe_path: &str) -> () {
self.path = exe_path.to_string();
}
pub fn set_clsid(&mut self, clsid: &str) -> () {
self.clsid = clsid.to_string();
}
pub fn set_name(&mut self, name: Option<&str>) -> () {
self.name = name.map(|n| n.to_string());
}
pub fn set_icon(&mut self, icon: Option<&str>) -> () {
self.icon = icon.map(|i| i.to_string());
}
pub fn set_behavior(&mut self, behavior: Option<&str>) -> () {
self.behavior = behavior.map(|b| b.to_string());
}
pub fn add_str(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
match self.strs.insert(name.to_string(), value.to_string()) {
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
None => Ok(()),
}
}
pub fn add_icon(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
match self.icons.insert(name.to_string(), value.to_string()) {
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
None => Ok(()),
}
}
pub fn add_behavior(&mut self, name: &str, value: &str) -> Result<(), SchemaError> {
match self.behaviors.insert(name.to_string(), value.to_string()) {
Some(_) => Err(SchemaError::DuplicateKey(name.to_string())),
None => Ok(()),
}
}
/// Add a file extension to the schema.
///
/// The parameter `ext` is the file extension without leading dot `.`.
pub fn add_ext(
&mut self,
ext: &str,
ext_name: &str,
ext_icon: &str,
ext_behavior: &str,
) -> Result<(), SchemaError> {
match self.exts.insert(
ext.to_string(),
SchemaExt::new(ext_name, ext_icon, ext_behavior),
) {
Some(_) => Err(SchemaError::DuplicateKey(ext.to_string())),
None => Ok(()),
}
}
}
impl Schema {
pub(super) fn get_identifier(&self) -> &str {
&self.identifier
}
pub(super) fn get_path(&self) -> &str {
&self.path
}
pub(super) fn get_clsid(&self) -> &str {
&self.clsid
}
pub(super) fn get_name(&self) -> Option<&str> {
self.name.as_ref().map(|v| v.as_str())
}
pub(super) fn get_icon(&self) -> Option<&str> {
self.icon.as_ref().map(|v| v.as_str())
}
pub(super) fn get_behavior(&self) -> Option<&str> {
self.behavior.as_ref().map(|v| v.as_str())
}
pub(super) fn get_strs(&self) -> &HashMap<String, String> {
&self.strs
}
pub(super) fn get_icons(&self) -> &HashMap<String, String> {
&self.icons
}
pub(super) fn get_behaviors(&self) -> &HashMap<String, String> {
&self.behaviors
}
pub(super) fn get_exts(&self) -> &HashMap<String, SchemaExt> {
&self.exts
}
}
// endregion
// region: Internal Stuff
/// Internal used struct as the Schema file extensions hashmap value type.
#[derive(Debug)]
pub(crate) struct SchemaExt {
name: String,
icon: String,
behavior: String,
}
impl SchemaExt {
fn new(name: &str, icon: &str, behavior: &str) -> Self {
Self {
name: name.to_string(),
icon: icon.to_string(),
behavior: behavior.to_string(),
}
}
}
impl SchemaExt {
pub(super) fn get_name(&self) -> &str {
&self.name
}
pub(super) fn get_icon(&self) -> &str {
&self.icon
}
pub(super) fn get_behavior(&self) -> &str {
&self.behavior
}
}
// endregion

View File

@@ -1,501 +0,0 @@
//! This crate provide utilities fetching and manilupating Windows file association.
//! All code under crate are following Microsoft document: https://learn.microsoft.com/en-us/windows/win32/shell/customizing-file-types-bumper
#[cfg(not(target_os = "windows"))]
compile_error!("Crate wfassoc is only supported on Windows.");
pub mod extra;
pub mod utilities;
pub mod assoc;
use assoc::{Ext, ProgId};
use indexmap::{IndexMap, IndexSet};
use regex::Regex;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::sync::LazyLock;
use thiserror::Error as TeError;
use winreg::RegKey;
use winreg::enums::{
HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE,
};
// region: Error Types
/// All possible error occurs in this crate.
#[derive(Debug, TeError)]
pub enum Error {
#[error("error occurs when manipulating with Registry: {0}")]
BadRegOper(#[from] std::io::Error),
#[error("{0}")]
CastOsStr(#[from] utilities::CastOsStrError),
#[error("{0}")]
ParseExt(#[from] assoc::ParseExtError),
#[error("no administrative privilege")]
NoPrivilege,
#[error("given identifier \"{0}\" of application is invalid")]
BadIdentifier(String),
#[error("given full path to application is invalid")]
BadFullAppPath,
#[error("manner \"{0}\" is already registered")]
DupManner(String),
#[error("file extension \"{0}\" is already registered")]
DupExt(String),
#[error("the token of manner is invalid")]
InvalidMannerToken,
#[error("the token of file extension is invalid")]
InvalidExtToken,
}
/// The result type used in this crate.
pub type Result<T> = std::result::Result<T, Error>;
// endregion
// region: Types
/// The token for access registered items in Program.
/// This is usually returned when you registering them.
pub type Token = usize;
/// The scope where wfassoc will register and unregister application.
#[derive(Debug, Copy, Clone)]
pub enum Scope {
/// Scope for current user.
User,
/// Scope for all users under this computer.
System,
}
/// The error occurs when cast View into Scope.
#[derive(Debug, TeError)]
#[error("hybrid View can not be cast into Scope")]
pub struct TryFromViewError {}
impl TryFromViewError {
fn new() -> Self {
Self {}
}
}
impl TryFrom<View> for Scope {
type Error = TryFromViewError;
fn try_from(value: View) -> std::result::Result<Self, Self::Error> {
match value {
View::User => Ok(Self::User),
View::System => Ok(Self::System),
View::Hybrid => Err(TryFromViewError::new()),
}
}
}
impl Scope {
/// Check whether we have enough privilege when operating in current scope.
/// If we have, return true, otherwise false.
pub fn has_privilege(&self) -> bool {
// If we operate on System, and we do not has privilege,
// we think we do not have privilege, otherwise,
// there is no privilege required.
!matches!(self, Self::System if !utilities::has_privilege())
}
}
/// The view when wfassoc querying file extension association.
#[derive(Debug, Copy, Clone)]
pub enum View {
/// The view of current user.
User,
/// The view of system.
System,
/// Hybrid view of User and System.
/// It can be seen as that we use System first and then use User to override any existing items.
Hybrid,
}
impl From<Scope> for View {
fn from(value: Scope) -> Self {
match value {
Scope::User => Self::User,
Scope::System => Self::System,
}
}
}
// endregion
// region: Program
/// The struct representing a complete program for registration and unregistration.
#[derive(Debug)]
pub struct Program {
/// The identifier of this program.
identifier: String,
/// The fully qualified path to the application.
full_path: PathBuf,
/// The collection holding all manners of this program.
manners: IndexSet<String>,
/// The collection holding all file extensions supported by this program.
/// The key is file estension and value is its associated manner for opening it.
exts: IndexMap<Ext, Token>,
}
impl Program {
/// Create a new registrar for following operations.
///
/// `identifier` is the unique name of this program.
/// If should only contain digits and alphabet chars,
/// and should not start with any digits.
/// For example, "MyApp" is okey but following names are not okey:
///
/// - `My App`
/// - `3DViewer`
/// - `我的Qt程序` (means "My Qt App" in English)
///
/// More preciously, `identifier` will be used as the vendor part of ProgId.
///
/// `full_path` is the fully qualified path to the application.
pub fn new(identifier: &str, full_path: &str) -> Result<Self> {
// Check identifier
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9]*$").unwrap());
if !RE.is_match(identifier) {
return Err(Error::BadIdentifier(identifier.to_string()));
}
// Everything is okey, build self.
Ok(Self {
identifier: identifier.to_string(),
// The error type of PathBuf FromStr trait is Infallible,
// so it must be okey and we can use unwrap safely.
full_path: full_path.parse().unwrap(),
manners: IndexSet::new(),
exts: IndexMap::new(),
})
}
/// Add manner provided by this program.
pub fn add_manner(&mut self, manner: &str) -> Result<Token> {
// TODO: Use wincmd::CmdArgs instead of String.
// Create manner from string
let manner = manner.to_string();
// Backup a stringfied manner for error output.
let manner_str = manner.to_string();
// Insert manner.
let idx = self.manners.len();
if self.manners.insert(manner) {
Ok(idx)
} else {
Err(Error::DupManner(manner_str))
}
}
/// Get the string display of manner represented by given token
pub fn get_manner_str(&self, token: Token) -> Option<String> {
self.manners.get_index(token).map(|s| s.clone())
}
/// Add file extension supported by this program and its associated manner.
pub fn add_ext(&mut self, ext: &str, token: Token) -> Result<Token> {
// Check manner token
if let None = self.manners.get_index(token) {
return Err(Error::InvalidMannerToken);
}
// Create extension from string
let ext = Ext::new(ext)?;
// Backup a stringfied extension for error output.
let ext_str = ext.to_string();
// Insert file extension
let idx = self.exts.len();
if let None = self.exts.insert(ext, token) {
Ok(idx)
} else {
Err(Error::DupExt(ext_str))
}
}
/// Get the string display of file extension represented by given token
pub fn get_ext_str(&self, token: Token) -> Option<String> {
self.exts.get_index(token).map(|p| p.0.to_string())
}
}
impl Program {
const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths";
const APPLICATIONS: &str = "Software\\Classes\\Applications";
/// Register this application.
pub fn register(&self, scope: Scope) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(Error::NoPrivilege);
}
// Fetch root key.
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
// Fetch file name and start in path.
let file_name = self.extract_file_name()?;
let start_in = self.extract_start_in()?;
// Create App Paths subkey
debug_println!("Adding App Paths subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?;
let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?;
// Write App Paths values
subkey.set_value("", &utilities::path_to_str(&self.full_path)?)?;
subkey.set_value("Path", &utilities::osstr_to_str(&start_in)?)?;
// Create Applications subkey
debug_println!("Adding Applications subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?;
// Write Applications values
if !self.exts.is_empty() {
let (supported_types, _) =
subkey.create_subkey_with_flags("SupportedTypes", KEY_WRITE)?;
for ext in self.exts.keys() {
supported_types.set_value(ext.to_string(), &"")?;
}
}
// Create ProgId subkeys
debug_println!("Adding ProgId subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
for (ext, manner_token) in self.exts.iter() {
let manner = self.manners.get_index(*manner_token).ok_or(Error::InvalidMannerToken)?;
let prog_id = self.build_prog_id(ext);
debug_println!("Adding ProgId \"{0}\" subkey...", prog_id.to_string());
let (subkey, _) = subkey_parent.create_subkey_with_flags(prog_id.to_string(), KEY_READ)?;
let (subkey_verb, _) = subkey.create_subkey_with_flags("open", KEY_READ)?;
let (subkey_command, _) = subkey_verb.create_subkey_with_flags("command", KEY_WRITE)?;
subkey_command.set_value("", manner)?;
}
// Okey
utilities::notify_assoc_changed();
Ok(())
}
/// Unregister this application.
pub fn unregister(&self, scope: Scope) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(Error::NoPrivilege);
}
// Fetch root key and file name.
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let file_name = self.extract_file_name()?;
// Remove App Paths subkey
debug_println!("Removing App Paths subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_WRITE)?;
subkey_parent.delete_subkey_all(file_name)?;
// Remove Applications subkey
debug_println!("Removing Applications subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
subkey_parent.delete_subkey_all(file_name)?;
// Remove ProgId subkeys
debug_println!("Removing ProgId subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
for ext in self.exts.keys() {
let prog_id = self.build_prog_id(ext);
debug_println!("Removing ProgId \"{0}\" subkey...", prog_id.to_string());
subkey_parent.delete_subkey_all(prog_id.to_string())?;
}
// Okey
utilities::notify_assoc_changed();
Ok(())
}
/// Check whether this application has been registered.
///
/// Please note that this is a rough check and do not validate any data.
pub fn is_registered(&self, scope: Scope) -> Result<bool> {
// Fetch root key and file name.
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let file_name = self.extract_file_name()?;
// Check App Paths subkey.
debug_println!("Checking App Paths subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?;
if let Err(_) = subkey_parent.open_subkey_with_flags(file_name, KEY_READ) {
return Ok(false);
}
// Check Application subkey.
debug_println!("Checking Applications subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
if let Err(_) = subkey_parent.open_subkey_with_flags(file_name, KEY_READ) {
return Ok(false);
}
// Both subkeys are roughly existing.
Ok(true)
}
}
impl Program {
const CLASSES: &str = "Software\\Classes";
/// Set the default "open with" of given token associated extension to this program.
pub fn link_ext(&self, ext: Token, scope: Scope) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(Error::NoPrivilege);
}
// Fetch file extension and build ProgId from it
let (ext, _) = match self.exts.get_index(ext) {
Some(v) => v,
None => return Err(Error::InvalidExtToken),
};
let prog_id = self.build_prog_id(ext);
// Fetch root key and navigate to Classes
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let classes = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
// Open or create this extension key
let (subkey, _) = classes.create_subkey_with_flags(ext.to_string(), KEY_WRITE)?;
// Set the default way to open this file extension
subkey.set_value("", &prog_id.to_string())?;
// Okey
Ok(())
}
/// Remove this program from the default "open with" of given token associated extension.
///
/// If the default "open with" of given extension is not our program,
/// or there is no such file extension, this function do nothing.
pub fn unlink_ext(&self, ext: Token, scope: Scope) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(Error::NoPrivilege);
}
// Fetch file extension and build ProgId from it
let (ext, _) = match self.exts.get_index(ext) {
Some(v) => v,
None => return Err(Error::InvalidExtToken),
};
let prog_id = self.build_prog_id(ext);
// Fetch root key and navigate to Classes
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
Scope::System => HKEY_LOCAL_MACHINE,
});
let classes = hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?;
// Open key for this extension.
// If there is no such key, return directly.
if let Some(subkey) =
extra::winreg::try_open_subkey_with_flags(&classes, ext.to_string(), KEY_WRITE)?
{
// Only delete the default key if it is equal to our ProgId
if let Some(value) = extra::winreg::try_get_value::<String, _>(&subkey, "")? {
if value == prog_id.to_string() {
// Delete the default key.
subkey.delete_value("")?;
}
}
}
// Okey
Ok(())
}
/// Query the default "open with" of given token associated extension.
///
/// This function will return its associated ProgId if it is existing.
pub fn query_ext(&self, ext: Token, view: View) -> Result<Option<ProgId>> {
// Fetch file extension
let (ext, _) = match self.exts.get_index(ext) {
Some(v) => v,
None => return Err(Error::InvalidExtToken),
};
// Fetch root key and navigate to Classes
let hk = match view {
View::User => RegKey::predef(HKEY_CURRENT_USER),
View::System => RegKey::predef(HKEY_LOCAL_MACHINE),
View::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
};
let classes = match view {
View::User | View::System => hk.open_subkey_with_flags(Self::CLASSES, KEY_READ)?,
View::Hybrid => hk.open_subkey_with_flags("", KEY_READ)?,
};
// Open key for this extension if possible
let rv =
match extra::winreg::try_open_subkey_with_flags(&classes, ext.to_string(), KEY_READ)? {
Some(subkey) => {
// Try get associated ProgId if possible
match extra::winreg::try_get_value::<String, _>(&subkey, "")? {
Some(value) => Some(ProgId::from(value.as_str())),
None => None,
}
}
None => None,
};
// Okey
Ok(rv)
}
}
impl Program {
/// Extract the file name part from full path to application,
/// which was used in Registry path component.
fn extract_file_name(&self) -> Result<&OsStr> {
// Get the file name part and make sure it is not empty.
// Empty checker is CRUCIAL!
self.full_path
.file_name()
.and_then(|p| if p.is_empty() { None } else { Some(p) })
.ok_or(Error::BadFullAppPath)
}
/// Extract the start in path from full path to application,
/// which basically is the stem of full path.
fn extract_start_in(&self) -> Result<&OsStr> {
// Get parent part and make sure it is not empty
// Empty checker is CRUCIAL!
self.full_path
.parent()
.map(|p| p.as_os_str())
.and_then(|p| if p.is_empty() { None } else { Some(p) })
.ok_or(Error::BadFullAppPath)
}
/// Build ProgId from identifier and given file extension.
fn build_prog_id(&self, ext: &Ext) -> ProgId {
ProgId::Std(assoc::StdProgId::new(
&self.identifier,
&utilities::capitalize_first_ascii(ext.inner()),
None,
))
}
}
// endregion

View File

@@ -1,582 +0,0 @@
/// The expand of winreg crate according to our module requirements.
mod winregex;
use regex::Regex;
use std::fmt::Display;
use std::str::FromStr;
use std::sync::LazyLock;
use thiserror::Error as TeError;
use winreg::RegKey;
// region: Error Types
/// All possible error occurs in this crate.
#[derive(Debug, TeError)]
pub enum Error {
#[error(
"can not register because lack essential privilege. please consider running with Administrator role"
)]
NoPrivilege,
#[error("{0}")]
Register(#[from] std::io::Error),
#[error("{0}")]
BadFileExt(#[from] ParseFileExtError),
#[error("{0}")]
BadProgId(#[from] ParseProgIdError),
}
// endregion
// region: Privilege, Scope and View
/// Check whether current process has administrative privilege.
///
/// It usually means that checking whether current process is running as Administrator.
/// Return true if it is, otherwise false.
///
/// Reference: https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership
pub fn has_privilege() -> bool {
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Security::{
AllocateAndInitializeSid, CheckTokenMembership, FreeSid, PSID, SECURITY_NT_AUTHORITY,
};
use windows_sys::Win32::System::SystemServices::{
DOMAIN_ALIAS_RID_ADMINS, SECURITY_BUILTIN_DOMAIN_RID,
};
use windows_sys::core::BOOL;
let nt_authority = SECURITY_NT_AUTHORITY.clone();
let mut administrators_group: PSID = PSID::default();
let success: BOOL = unsafe {
AllocateAndInitializeSid(
&nt_authority,
2,
SECURITY_BUILTIN_DOMAIN_RID as u32,
DOMAIN_ALIAS_RID_ADMINS as u32,
0,
0,
0,
0,
0,
0,
&mut administrators_group,
)
};
if success == 0 {
panic!("Win32 AllocateAndInitializeSid() failed");
}
let mut is_member: BOOL = BOOL::default();
let success: BOOL =
unsafe { CheckTokenMembership(HANDLE::default(), administrators_group, &mut is_member) };
unsafe {
FreeSid(administrators_group);
}
if success == 0 {
panic!("Win32 CheckTokenMembership() failed");
}
is_member != 0
}
/// The scope where wfassoc will register and unregister.
#[derive(Debug, Copy, Clone)]
pub enum Scope {
/// Scope for current user.
User,
/// Scope for all users under this computer.
System,
}
/// The view when wfassoc querying infomations.
#[derive(Debug, Copy, Clone)]
pub enum View {
/// The view of current user.
User,
/// The view of system.
System,
/// Hybrid view of User and System.
/// It can be seen as that we use System first and then use User to override any existing items.
Hybrid,
}
/// The error occurs when cast View into Scope.
#[derive(Debug, TeError)]
#[error("hybrid view can not be cast into any scope")]
pub struct TryFromViewError {}
impl TryFromViewError {
fn new() -> Self {
Self {}
}
}
impl From<Scope> for View {
fn from(value: Scope) -> Self {
match value {
Scope::User => Self::User,
Scope::System => Self::System,
}
}
}
impl TryFrom<View> for Scope {
type Error = TryFromViewError;
fn try_from(value: View) -> Result<Self, Self::Error> {
match value {
View::User => Ok(Self::User),
View::System => Ok(Self::System),
View::Hybrid => Err(TryFromViewError::new()),
}
}
}
impl Scope {
/// Check whether we have enough privilege when operating in current scope.
/// If we have, simply return, otherwise return error.
fn check_privilege(&self) -> Result<(), Error> {
if matches!(self, Self::System if !has_privilege()) {
Err(Error::NoPrivilege)
} else {
Ok(())
}
}
}
// endregion
// region: File Extension
/// The struct representing an file extension which must start with dot (`.`)
/// and followed by at least one arbitrary characters.
#[derive(Debug, Clone)]
pub struct FileExt {
/// The body of file extension (excluding dot).
inner: String,
}
impl FileExt {
pub fn new(file_ext: &str) -> Result<Self, ParseFileExtError> {
Self::from_str(file_ext)
}
}
/// The error occurs when try parsing string into FileExt.
#[derive(Debug, TeError)]
#[error("given file extension is invalid")]
pub struct ParseFileExtError {}
impl ParseFileExtError {
fn new() -> Self {
Self {}
}
}
impl Display for FileExt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, ".{}", self.inner)
}
}
impl FromStr for FileExt {
type Err = ParseFileExtError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\.([^\.]+)$").unwrap());
match RE.captures(s) {
Some(v) => Ok(Self {
inner: v[1].to_string(),
}),
None => Err(ParseFileExtError::new()),
}
}
}
impl FileExt {
fn open_scope(&self, scope: Scope) -> Result<RegKey, Error> {
use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE};
// check privilege
scope.check_privilege()?;
// get the root key
let hk = match scope {
Scope::User => RegKey::predef(HKEY_CURRENT_USER),
Scope::System => RegKey::predef(HKEY_LOCAL_MACHINE),
};
// navigate to classes
let classes = hk.open_subkey_with_flags("Software\\Classes", KEY_READ | KEY_WRITE)?;
// okey
Ok(classes)
}
fn open_view(&self, view: View) -> Result<Option<RegKey>, Error> {
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ};
// navigate to extension container
let hk = match view {
View::User => RegKey::predef(HKEY_CURRENT_USER),
View::System => RegKey::predef(HKEY_LOCAL_MACHINE),
View::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
};
let classes = match view {
View::User | View::System => {
hk.open_subkey_with_flags("Software\\Classes", KEY_READ)?
}
View::Hybrid => hk.open_subkey_with_flags("", KEY_READ)?,
};
// check whether there is this ext
classes.
// open extension key if possible
let thisext = classes.open_subkey_with_flags(file_ext.to_string(), KEY_READ)?;
// okey
Ok(classes)
}
pub fn get_current(&self, view: View) -> Option<ProgId> {
todo!()
}
pub fn set_current(&mut self, scope: Scope, prog_id: Option<&ProgId>) -> Result<(), Error> {
scope.check_privilege()?;
todo!()
}
pub fn iter_open_with(&self, view: View) -> Result<impl Iterator<Item = ProgId>, Error> {
let viewer = match self.open_view(view)? {
Some(viewer) => viewer,
None => return Ok(std::iter::empty::<ProgId>()),
};
let it = winregex::iter_sz_keys(&viewer);
let it = winregex::exclude_default_key(it);
Ok(it.map(|s| ProgId::from(s.as_str())))
}
pub fn insert_open_with(&mut self, scope: Scope, prog_id: &ProgId) -> Result<(), Error> {
scope.check_privilege()?;
todo!()
}
pub fn flash_open_with(
&mut self,
scope: Scope,
prog_ids: impl Iterator<Item = ProgId>,
) -> Result<(), Error> {
scope.check_privilege()?;
todo!()
}
}
/// The association infomations of specific file extension.
#[derive(Debug)]
pub struct FileExtAssoc {
default: String,
open_with_progids: Vec<String>,
}
impl FileExtAssoc {
fn new(file_ext: &FileExt, view: View) -> Option<Self> {
use winreg::RegKey;
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ};
// navigate to extension container
let hk = match view {
View::User => RegKey::predef(HKEY_CURRENT_USER),
View::System => RegKey::predef(HKEY_LOCAL_MACHINE),
View::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
};
let classes = match view {
View::User | View::System => hk
.open_subkey_with_flags("Software\\Classes", KEY_READ)
.unwrap(),
View::Hybrid => hk.open_subkey_with_flags("", KEY_READ).unwrap(),
};
// open extension key if possible
let thisext = match classes.open_subkey_with_flags(file_ext.to_string(), KEY_READ) {
Ok(v) => v,
Err(_) => return None,
};
// fetch extension infos.
let default = thisext.get_value("").unwrap_or(String::new());
let open_with_progids =
if let Ok(progids) = thisext.open_subkey_with_flags("OpenWithProdIds", KEY_READ) {
progids
.enum_keys()
.map(|x| x.unwrap())
.filter(|k| !k.is_empty())
.collect()
} else {
Vec::new()
};
Some(Self {
default,
open_with_progids,
})
}
pub fn get_default(&self) -> &str {
&self.default
}
pub fn len_open_with_progid(&self) -> usize {
self.open_with_progids.len()
}
pub fn iter_open_with_progids(&self) -> impl Iterator<Item = &str> {
self.open_with_progids.iter().map(|s| s.as_str())
}
}
// endregion
// region: Executable Resource
// /// The struct representing an Windows executable resources path like
// /// `path_to_file.exe,1`.
// pub struct ExecRc {
// /// The path to binary for finding resources.
// binary: PathBuf,
// /// The inner index of resources.
// index: u32,
// }
// impl ExecRc {
// pub fn new(res_str: &str) -> Result<Self, ParseExecRcError> {
// static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^([^,]+),([0-9]+)$").unwrap());
// let caps = RE.captures(res_str);
// if let Some(caps) = caps {
// let binary = PathBuf::from_str(&caps[1])?;
// let index = caps[2].parse::<u32>()?;
// Ok(Self { binary, index })
// } else {
// Err(ParseExecRcError::NoCapture)
// }
// }
// }
// /// The error occurs when try parsing string into ExecRc.
// #[derive(Debug, TeError)]
// #[error("given string is not a valid executable resource string")]
// pub enum ParseExecRcError {
// /// Given string is not matched with format.
// NoCapture,
// /// Fail to convert executable part into path.
// BadBinaryPath(#[from] std::convert::Infallible),
// /// Fail to convert index part into valid number.
// BadIndex(#[from] std::num::ParseIntError),
// }
// impl FromStr for ExecRc {
// type Err = ParseExecRcError;
// fn from_str(s: &str) -> Result<Self, Self::Err> {
// ExecRc::new(s)
// }
// }
// impl Display for ExecRc {
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// write!(f, "{},{}", self.binary.to_str().unwrap(), self.index)
// }
// }
// endregion
// region: Programmatic Identifiers (ProgId)
/// The struct representing Programmatic Identifiers (ProgId).
///
/// Because there is optional part in standard ProgId, and not all software developers
/// are willing to following Microsoft suggestions, there is no strict constaint for ProgId.
/// So this struct is actually an enum which holding any possible ProgId format.
///
/// Reference: https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids
pub enum ProgId {
Plain(String),
Loose(LosseProgId),
Strict(StrictProgId),
}
impl From<&str> for ProgId {
fn from(s: &str) -> Self {
// match it for strict ProgId first
if let Ok(v) = StrictProgId::from_str(s) {
return Self::Strict(v);
}
// then match for loose ProgId
if let Ok(v) = LosseProgId::from_str(s) {
return Self::Loose(v);
}
// fallback with plain
Self::Plain(s.to_string())
}
}
impl Display for ProgId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProgId::Plain(v) => v.fmt(f),
ProgId::Loose(v) => v.fmt(f),
ProgId::Strict(v) => v.fmt(f),
}
}
}
/// The error occurs when parsing ProgId.
#[derive(Debug, TeError)]
#[error("given ProgId string is invalid")]
pub struct ParseProgIdError {}
impl ParseProgIdError {
fn new() -> Self {
Self {}
}
}
/// The ProgId similar with strict ProgId, but no version part.
pub struct LosseProgId {
vendor: String,
component: String,
}
impl LosseProgId {
pub fn new(vendor: &str, component: &str) -> Self {
Self {
vendor: vendor.to_string(),
component: component.to_string(),
}
}
pub fn get_vendor(&self) -> &str {
&self.vendor
}
pub fn get_component(&self) -> &str {
&self.component
}
}
impl FromStr for LosseProgId {
type Err = ParseProgIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
static RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^([a-zA-Z0-9]+)\.([a-zA-Z0-9]+)$").unwrap());
let caps = RE.captures(s);
if let Some(caps) = caps {
let vendor = &caps[1];
let component = &caps[2];
Ok(Self::new(vendor, component))
} else {
Err(ParseProgIdError::new())
}
}
}
impl Display for LosseProgId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", self.vendor, self.component)
}
}
/// The ProgId exactly follows `[Vendor or Application].[Component].[Version]` format.
pub struct StrictProgId {
vendor: String,
component: String,
version: u32,
}
impl StrictProgId {
pub fn new(vendor: &str, component: &str, version: u32) -> Self {
Self {
vendor: vendor.to_string(),
component: component.to_string(),
version,
}
}
pub fn get_vendor(&self) -> &str {
&self.vendor
}
pub fn get_component(&self) -> &str {
&self.component
}
pub fn get_version(&self) -> u32 {
self.version
}
}
impl FromStr for StrictProgId {
type Err = ParseProgIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
static RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^([a-zA-Z0-9]+)\.([a-zA-Z0-9]+)\.([0-9]+)$").unwrap());
let caps = RE.captures(s);
if let Some(caps) = caps {
let vendor = &caps[1];
let component = &caps[2];
let version = caps[3]
.parse::<u32>()
.map_err(|_| ParseProgIdError::new())?;
Ok(Self::new(vendor, component, version))
} else {
Err(ParseProgIdError::new())
}
}
}
impl Display for StrictProgId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}", self.vendor, self.component, self.version)
}
}
// endregion
// region: Program
// /// The struct representing a complete Win32 program.
// pub struct Program {
// file_exts: Vec<FileExt>,
// }
// impl Program {
// /// Create a program descriptor.
// pub fn new() -> Self {
// Self {
// file_exts: Vec::new(),
// }
// }
// }
// impl Program {
// /// Register program in this computer
// pub fn register(&self, kind: RegisterKind) -> Result<(), Error> {
// todo!("pretend to register >_<...")
// }
// /// Unregister program from this computer.
// pub fn unregister(&self) -> Result<(), Error> {
// todo!("pretend to unregister >_<...")
// }
// }
// impl Program {
// /// Query file extension infos which this program want to associate with.
// pub fn query(&self) -> Result<(), Error> {
// todo!("pretend to query >_<...")
// }
// }
// endregion

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,148 @@
use super::{
Error, OpenKeyPurpose, OpenKeyTerritory, OpenedKey, PERM_RW, Result, Scope, check_privilege,
};
use crate::win32::{concept, regext};
use winreg::RegKey;
use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AppPathsKey {
key_name: concept::FileName,
}
impl AppPathsKey {
pub fn new(inner: concept::FileName) -> Self {
Self { key_name: inner }
}
pub fn inner(&self) -> &concept::FileName {
&self.key_name
}
}
impl AppPathsKey {
const APP_PATHS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths";
fn open_key(&self, territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<OpenedKey> {
// check privilege
check_privilege(territory, purpose)?;
// Fetch the permission
let perms = purpose.to_permission();
// get the root key
let hk = match territory {
OpenKeyTerritory::User => RegKey::predef(HKEY_CURRENT_USER),
OpenKeyTerritory::System => RegKey::predef(HKEY_LOCAL_MACHINE),
// There is no way that territory is hybrid.
OpenKeyTerritory::Hybrid => panic!("unexpected hybrid key territory"),
};
// navigate to App Paths
let app_paths = hk.open_subkey_with_flags(Self::APP_PATHS, perms)?;
// open file name key if possible
let this_app = regext::try_open_subkey_with_flags(
&app_paths,
regext::blank_path_guard(self.key_name.inner())?,
perms,
)?;
// okey
Ok(OpenedKey::new(app_paths, this_app))
}
fn open_scope_for_read(&self, scope: Scope) -> Result<OpenedKey> {
self.open_key(scope.into(), OpenKeyPurpose::Read)
}
fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey> {
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite)
}
pub fn is_exist(&self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_read(scope)?.this_key;
Ok(key.is_some())
}
/// Ensure this application key is presented in App Paths key.
///
/// Return true if we newly create this key,
/// otherwise false indicating there already is an existing key.
pub fn ensure(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?;
if let None = key.this_key {
let _ = key.parent_key.create_subkey_with_flags(
regext::blank_path_guard(self.key_name.inner())?,
PERM_RW,
)?;
Ok(true)
} else {
Ok(false)
}
}
/// Delete this application key from App Paths key.
///
/// Return true if we successfully delete this key,
/// otherwise false indicating there is no such key (already deleted).
pub fn delete(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?;
Ok(regext::arbitrarily_delete_subkey_all(
&key.parent_key,
regext::blank_path_guard(self.key_name.inner())?,
)?)
}
fn open_scope_for_getter(&self, scope: Scope) -> Result<RegKey> {
self.open_scope_for_read(scope)?
.this_key
.ok_or(Error::InexistantKey)
}
fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey> {
self.open_scope_for_write(scope)?
.this_key
.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 = "";
///
///
/// This field point to the fully qualified path to the application.
pub fn get_default(&self, scope: Scope) -> Result<String> {
let key = self.open_scope_for_getter(scope)?;
Ok(key.get_value(Self::NAMEOF_DEFAULT)?)
}
///
///
/// This field should be filled with fully qualified path to the application.
pub fn set_default(&mut self, scope: Scope, value: &str) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
key.set_value(Self::NAMEOF_DEFAULT, &value)?;
Ok(())
}
const NAMEOF_PATH: &str = "Path";
///
///
/// This field point to the added path for PATH environment variable.
/// Usually it is the path to application directory.
pub fn get_path(&self, scope: Scope) -> Result<String> {
let key = self.open_scope_for_getter(scope)?;
Ok(key.get_value(Self::NAMEOF_PATH)?)
}
///
///
/// This field should be the added path for PATH environment variable.
/// Usually it is the path to application directory.
pub fn set_path(&mut self, scope: Scope, value: &str) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
key.set_value(Self::NAMEOF_PATH, &value)?;
Ok(())
}
}

View File

@@ -0,0 +1,336 @@
use super::{
Error, IconResVariant, OpenKeyPurpose, OpenKeyTerritory, OpenedKey, PERM_R, PERM_RW, Result,
Scope, ShellVerb, StrResVariant, View, check_privilege,
};
use crate::win32::{concept, regext};
use winreg::RegKey;
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ApplicationsKey {
key_name: concept::FileName,
}
impl ApplicationsKey {
pub fn new(inner: concept::FileName) -> Self {
Self { key_name: inner }
}
pub fn inner(&self) -> &concept::FileName {
&self.key_name
}
}
impl ApplicationsKey {
const FULL_APPLICATIONS: &str = "Software\\Classes\\Applications";
const PARTIAL_APPLICATIONS: &str = "Applications";
fn open_key(&self, territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<OpenedKey> {
// check privilege
check_privilege(territory, purpose)?;
// Fetch the permission
let perms = purpose.to_permission();
// get the root key
let hk = match territory {
OpenKeyTerritory::User => RegKey::predef(HKEY_CURRENT_USER),
OpenKeyTerritory::System => RegKey::predef(HKEY_LOCAL_MACHINE),
OpenKeyTerritory::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
};
let applications = match territory {
OpenKeyTerritory::User | OpenKeyTerritory::System => {
hk.open_subkey_with_flags(Self::FULL_APPLICATIONS, perms)?
}
OpenKeyTerritory::Hybrid => {
hk.open_subkey_with_flags(Self::PARTIAL_APPLICATIONS, perms)?
}
};
// open app key if possible
let this_app = regext::try_open_subkey_with_flags(
&applications,
regext::blank_path_guard(self.key_name.inner())?,
perms,
)?;
// okey
Ok(OpenedKey::new(applications, this_app))
}
fn open_view_for_read(&self, view: View) -> Result<OpenedKey> {
self.open_key(view.into(), OpenKeyPurpose::Read)
}
fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey> {
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite)
}
pub fn is_exist(&self, view: View) -> Result<bool> {
let key = self.open_view_for_read(view)?.this_key;
Ok(key.is_some())
}
/// Ensure this application key is presented in Applications key.
///
/// Return true if we newly create this key,
/// otherwise false indicating there already is an existing key.
pub fn ensure(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?;
if let None = key.this_key {
let _ = key.parent_key.create_subkey_with_flags(
regext::blank_path_guard(self.key_name.inner())?,
PERM_RW,
)?;
Ok(true)
} else {
Ok(false)
}
}
/// Delete this application key from Applications key.
///
/// Return true if we successfully delete this key,
/// otherwise false indicating there is no such key (already deleted).
pub fn delete(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?;
Ok(regext::arbitrarily_delete_subkey_all(
&key.parent_key,
regext::blank_path_guard(self.key_name.inner())?,
)?)
}
// YYC MARK:
// Reference: https://learn.microsoft.com/en-us/windows/win32/shell/app-registration#using-the-applications-subkey
fn open_view_for_getter(&self, view: View) -> Result<RegKey> {
self.open_view_for_read(view)?
.this_key
.ok_or(Error::InexistantKey)
}
fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey> {
self.open_scope_for_write(scope)?
.this_key
.ok_or(Error::InexistantKey)
}
const NAMEOF_SHELL_VERB_PART1: &str = "shell";
const NAMEOF_SHELL_VERB_PART3: &str = "command";
const NAMEOF_SHELL_VERB_PART4: &str = "";
pub fn get_shell_verb(&self, view: View) -> Result<Option<ShellVerb>> {
let key = self.open_view_for_getter(view)?;
// Get shell subkey
let shell_key = match regext::try_open_subkey_with_flags(
&key,
Self::NAMEOF_SHELL_VERB_PART1,
PERM_R,
)? {
Some(key) => key,
None => return Ok(None),
};
// Get verb subkey name, then get subkey itself.
let verb_key_name = match regext::get_sole_subkey_name(&shell_key)? {
Some(key) => key,
None => return Ok(None),
};
let verb_key = match regext::try_open_subkey_with_flags(
&shell_key,
regext::blank_path_guard(&verb_key_name)?,
PERM_R,
)? {
Some(key) => key,
None => return Ok(None),
};
// Get command subkey.
let command_key = match regext::try_open_subkey_with_flags(
&verb_key,
Self::NAMEOF_SHELL_VERB_PART3,
PERM_R,
)? {
Some(key) => key,
None => return Ok(None),
};
// Get the default value of command subkey
let command_default_value = match regext::try_get_value::<String, _>(
&command_key,
Self::NAMEOF_SHELL_VERB_PART4,
)? {
Some(value) => value,
None => return Ok(None),
};
// Okey, return value.
Ok(Some(ShellVerb::new(
verb_key_name.parse()?,
command_default_value.parse()?,
)))
}
pub fn set_shell_verb(&mut self, scope: Scope, sv: Option<&ShellVerb>) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
match sv {
Some(sv) => {
// Create shell subkey
let (shell_key, _) =
key.create_subkey_with_flags(Self::NAMEOF_SHELL_VERB_PART1, PERM_RW)?;
// Create verb key
let (verb_key, _) =
shell_key.create_subkey_with_flags(sv.get_verb().inner(), PERM_RW)?;
// Create command key
let (command_key, _) =
verb_key.create_subkey_with_flags(Self::NAMEOF_SHELL_VERB_PART3, PERM_RW)?;
// Set command key default value
command_key.set_value(Self::NAMEOF_SHELL_VERB_PART4, &sv.get_command().full())?;
}
None => {
// Delete shell and its all subkey.
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_SHELL_VERB_PART1)?;
}
}
Ok(())
}
const NAMEOF_DEFAULT_ICON_PART1: &str = "DefaultIcon";
const NAMEOF_DEFAULT_ICON_PART2: &str = "";
pub fn get_default_icon(&self, view: View) -> Result<Option<IconResVariant>> {
let key = self.open_view_for_getter(view)?;
// Get default icon subkey
let default_icon_key = match regext::try_open_subkey_with_flags(
&key,
Self::NAMEOF_DEFAULT_ICON_PART1,
PERM_R,
)? {
Some(key) => key,
None => return Ok(None),
};
// Get the default value of default icon subkey
let default_icon_default_value =
regext::try_get_value::<String, _>(&default_icon_key, Self::NAMEOF_DEFAULT_ICON_PART2)?;
// Transform it as result
Ok(default_icon_default_value.map(|v| IconResVariant::from(v.as_str())))
}
pub fn set_default_icon(&mut self, scope: Scope, icon: Option<&IconResVariant>) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
match icon {
Some(icon) => {
// Create default icon subkey
let (default_icon_key, _) =
key.create_subkey_with_flags(Self::NAMEOF_DEFAULT_ICON_PART1, PERM_RW)?;
// Set default value of default icon subkey.
default_icon_key.set_value(Self::NAMEOF_DEFAULT_ICON_PART2, &icon.to_string())?;
}
None => {
// Delete shell and its all subkey.
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_DEFAULT_ICON_PART1)?;
}
}
Ok(())
}
const NAMEOF_FRIENDLY_APP_NAME: &str = "FriendlyAppName";
pub fn get_friendly_app_name(&self, view: View) -> Result<Option<StrResVariant>> {
let key = self.open_view_for_getter(view)?;
// Get value of it
let value = regext::try_get_value::<String, _>(&key, Self::NAMEOF_FRIENDLY_APP_NAME)?;
// Transform it as result
Ok(value.map(|v| StrResVariant::from(v.as_str())))
}
pub fn set_friendly_app_name(
&mut self,
scope: Scope,
name: Option<&StrResVariant>,
) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
match name {
Some(name) => {
// Set value for this key
key.set_value(Self::NAMEOF_FRIENDLY_APP_NAME, &name.to_string())?;
}
None => {
// Delete this key
regext::arbitrarily_delete_value(&key, Self::NAMEOF_FRIENDLY_APP_NAME)?;
}
}
Ok(())
}
const NAMEOF_SUPPORTED_TYPES: &str = "SupportedTypes";
pub fn get_supported_types(&self, view: View) -> Result<Option<Vec<concept::Ext>>> {
let key = self.open_view_for_getter(view)?;
// Get supported types subkey
let supported_types_key =
match regext::try_open_subkey_with_flags(&key, Self::NAMEOF_SUPPORTED_TYPES, PERM_R)? {
Some(key) => key,
None => return Ok(None),
};
// Fetch all sub-values
let key_names = regext::get_all_string_subkey_names(&supported_types_key)?;
// Map the result
let exts = key_names
.into_iter()
.map(|name| name.parse::<concept::Ext>())
.collect::<std::result::Result<Vec<_>, _>>()?;
// Return value
Ok(Some(exts))
}
pub fn set_supported_types(
&mut self,
scope: Scope,
tys: Option<&[&concept::Ext]>,
) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
match tys {
Some(tys) => {
// Create supported types key
let (supported_types_key, _) =
key.create_subkey_with_flags(Self::NAMEOF_SUPPORTED_TYPES, PERM_RW)?;
// Clean all contents of this key
regext::clean_all_contents(&supported_types_key)?;
// Add file types one by one
for ty in tys {
supported_types_key
.set_value(regext::blank_path_guard(ty.dotted_inner())?, &"")?;
}
}
None => {
// Delete this subkey.
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_SUPPORTED_TYPES)?;
}
}
Ok(())
}
const NAMEOF_NO_OPEN_WITH: &str = "NoOpenWith";
pub fn get_no_open_with(&self, view: View) -> Result<bool> {
let key = self.open_view_for_getter(view)?;
match regext::try_get_value::<String, _>(&key, Self::NAMEOF_NO_OPEN_WITH)? {
Some(_) => Ok(true),
None => Ok(false),
}
}
pub fn set_no_open_with(&mut self, scope: Scope, flag: bool) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
if flag {
key.set_value(Self::NAMEOF_NO_OPEN_WITH, &"")?;
} else {
regext::arbitrarily_delete_value(&key, Self::NAMEOF_NO_OPEN_WITH)?;
}
Ok(())
}
}

View File

@@ -0,0 +1,223 @@
use super::{
Error, LosseProgId, OpenKeyPurpose, OpenKeyTerritory, OpenedKey, PERM_R, PERM_RW, Result,
Scope, View, check_privilege,
};
use crate::win32::{concept, regext};
use winreg::RegKey;
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ExtKey {
ext: concept::Ext,
}
impl ExtKey {
pub fn new(inner: concept::Ext) -> Self {
Self { ext: inner }
}
pub fn inner(&self) -> &concept::Ext {
&self.ext
}
}
impl ExtKey {
const FULL_CLASSES: &str = "Software\\Classes";
const PARTIAL_CLASSES: &str = "";
fn open_key(&self, territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<OpenedKey> {
// check privilege
check_privilege(territory, purpose)?;
// Fetch the permission
let perms = purpose.to_permission();
// navigate to extension container
let hk = match territory {
OpenKeyTerritory::User => RegKey::predef(HKEY_CURRENT_USER),
OpenKeyTerritory::System => RegKey::predef(HKEY_LOCAL_MACHINE),
OpenKeyTerritory::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
};
let classes = match territory {
OpenKeyTerritory::User | OpenKeyTerritory::System => {
hk.open_subkey_with_flags(Self::FULL_CLASSES, perms)?
}
OpenKeyTerritory::Hybrid => hk.open_subkey_with_flags(Self::PARTIAL_CLASSES, perms)?,
};
// open extension key if possible
let this_ext = regext::try_open_subkey_with_flags(
&classes,
regext::blank_path_guard(self.ext.dotted_inner())?,
perms,
)?;
// okey
Ok(OpenedKey::new(classes, this_ext))
}
fn open_view_for_read(&self, view: View) -> Result<OpenedKey> {
self.open_key(view.into(), OpenKeyPurpose::Read)
}
fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey> {
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite)
}
pub fn is_exist(&self, view: View) -> Result<bool> {
let key = self.open_view_for_read(view)?.this_key;
Ok(key.is_some())
}
/// Ensure this file extension key is presented in Classes key.
///
/// Return true if we newly create this key,
/// otherwise false indicating there already is an existing key.
pub fn ensure(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?;
if let None = key.this_key {
let _ = key.parent_key.create_subkey_with_flags(
regext::blank_path_guard(self.ext.dotted_inner())?,
PERM_RW,
)?;
Ok(true)
} else {
Ok(false)
}
}
/// Delete this file extension key from Classes key.
///
/// Return true if we successfully delete this key,
/// otherwise false indicating there is no such key (already deleted).
pub fn delete(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?;
Ok(regext::arbitrarily_delete_subkey_all(
&key.parent_key,
regext::blank_path_guard(self.ext.dotted_inner())?,
)?)
}
// YYC MARK:
// Reference: https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types#setting-optional-subkeys-and-file-type-extension-attributes
// TODO:
// We do not support "Content Type" and "PerceivedType"
// because current interface are enough to use,
// and these types has not been made as concept struct in Rust.
// We may expand these in future.
fn open_view_for_getter(&self, view: View) -> Result<RegKey> {
self.open_view_for_read(view)?
.this_key
.ok_or(Error::InexistantKey)
}
fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey> {
self.open_scope_for_write(scope)?
.this_key
.ok_or(Error::InexistantKey)
}
const NAMEOF_DEFAULT: &str = "";
pub fn get_default(&self, view: View) -> Result<Option<LosseProgId>> {
let key = self.open_view_for_getter(view)?;
// Get value of it
let value = regext::try_get_value::<String, _>(&key, Self::NAMEOF_DEFAULT)?;
// Transform it as result
Ok(value.map(|v| LosseProgId::from(v.as_str())))
}
pub fn set_default(&mut self, scope: Scope, pid: Option<&LosseProgId>) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
match pid {
Some(pid) => {
// Set value for this key
key.set_value(Self::NAMEOF_DEFAULT, &pid.to_string())?;
}
None => {
// Delete this key
regext::arbitrarily_delete_value(&key, Self::NAMEOF_DEFAULT)?;
}
}
Ok(())
}
const NAMEOF_OPEN_WITH_PROGIDS: &str = "OpenWithProgIds";
pub fn get_open_with_progids(&self, view: View) -> Result<Option<Vec<LosseProgId>>> {
let key = self.open_view_for_getter(view)?;
// Get OpenWithProgIds subkey
let open_with_progids_key =
match regext::try_open_subkey_with_flags(&key, Self::NAMEOF_OPEN_WITH_PROGIDS, PERM_R)?
{
Some(key) => key,
None => return Ok(None),
};
// Fetch all sub-values
let key_names = regext::get_all_string_subkey_names(&open_with_progids_key)?;
// Map the result
let progids = key_names
.into_iter()
.map(|name| LosseProgId::from(name.as_str()))
.collect();
// Return value
Ok(Some(progids))
}
///
///
/// If there is no "OpenWithProgIds" subkey, this function return false.
pub fn is_in_open_with_progids(&self, view: View, pid: &LosseProgId) -> Result<bool> {
let key = self.open_view_for_getter(view)?;
// Get OpenWithProgIds subkey
let open_with_progids_key =
match regext::try_open_subkey_with_flags(&key, Self::NAMEOF_OPEN_WITH_PROGIDS, PERM_R)?
{
Some(key) => key,
None => return Ok(false),
};
// Check whether there is given ProgId
Ok(regext::try_get_value::<String, _>(
&open_with_progids_key,
regext::blank_path_guard(pid.to_string())?,
)?
.is_some())
}
///
///
/// If there is no "OpenWithProgIds" subkey, this function will create it first,
/// then add your given ProgId into it.
pub fn add_into_open_with_progids(&mut self, scope: Scope, pid: &LosseProgId) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
// Get subkey
let (open_with_progids_key, _) =
key.create_subkey_with_flags(Self::NAMEOF_OPEN_WITH_PROGIDS, PERM_RW)?;
// Add value
open_with_progids_key.set_value(regext::blank_path_guard(pid.to_string())?, &"")?;
Ok(())
}
///
///
/// If there is no "OpenWithProgIds" subkey, this function do nothing.
pub fn remove_from_open_with_progids(&mut self, scope: Scope, pid: &LosseProgId) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
// Try get subkey
let open_with_progids_key = match regext::try_open_subkey_with_flags(
&key,
Self::NAMEOF_OPEN_WITH_PROGIDS,
PERM_RW,
)? {
Some(key) => key,
None => return Ok(()),
};
// Remove given key
regext::arbitrarily_delete_value(
&open_with_progids_key,
regext::blank_path_guard(pid.to_string())?,
)?;
Ok(())
}
}

View File

@@ -0,0 +1,302 @@
use super::{
Error, IconResVariant, LosseProgId, OpenKeyPurpose, OpenKeyTerritory, OpenedKey, PERM_R,
PERM_RW, Result, Scope, ShellVerb, StrResVariant, View, check_privilege,
};
use crate::win32::regext;
use winreg::RegKey;
use winreg::enums::{HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ProgIdKey {
progid: LosseProgId,
}
impl ProgIdKey {
pub fn new(inner: LosseProgId) -> Self {
Self { progid: inner }
}
pub fn inner(&self) -> &LosseProgId {
&self.progid
}
}
impl ProgIdKey {
const FULL_CLASSES: &str = "Software\\Classes";
const PARTIAL_CLASSES: &str = "";
fn open_key(&self, territory: OpenKeyTerritory, purpose: OpenKeyPurpose) -> Result<OpenedKey> {
// check privilege
check_privilege(territory, purpose)?;
// Fetch the permission
let perms = purpose.to_permission();
// navigate to ProgId container
let hk = match territory {
OpenKeyTerritory::User => RegKey::predef(HKEY_CURRENT_USER),
OpenKeyTerritory::System => RegKey::predef(HKEY_LOCAL_MACHINE),
OpenKeyTerritory::Hybrid => RegKey::predef(HKEY_CLASSES_ROOT),
};
let classes = match territory {
OpenKeyTerritory::User | OpenKeyTerritory::System => {
hk.open_subkey_with_flags(Self::FULL_CLASSES, perms)?
}
OpenKeyTerritory::Hybrid => hk.open_subkey_with_flags(Self::PARTIAL_CLASSES, perms)?,
};
// open ProgId key if possible
let this_progid = regext::try_open_subkey_with_flags(
&classes,
regext::blank_path_guard(self.progid.to_string())?,
perms,
)?;
// okey
Ok(OpenedKey::new(classes, this_progid))
}
fn open_view_for_read(&self, view: View) -> Result<OpenedKey> {
self.open_key(view.into(), OpenKeyPurpose::Read)
}
fn open_scope_for_write(&self, scope: Scope) -> Result<OpenedKey> {
self.open_key(scope.into(), OpenKeyPurpose::ReadWrite)
}
pub fn is_exist(&self, view: View) -> Result<bool> {
let key = self.open_view_for_read(view)?.this_key;
Ok(key.is_some())
}
/// Ensure this ProgId key is presented in Classes key.
///
/// Return true if we newly create this key,
/// otherwise false indicating there already is an existing key.
pub fn ensure(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?;
if let None = key.this_key {
let _ = key.parent_key.create_subkey_with_flags(
regext::blank_path_guard(self.progid.to_string())?,
PERM_RW,
)?;
Ok(true)
} else {
Ok(false)
}
}
/// Delete this ProgId key from Classes key.
///
/// Return true if we successfully delete this key,
/// otherwise false indicating there is no such key (already deleted).
pub fn delete(&mut self, scope: Scope) -> Result<bool> {
let key = self.open_scope_for_write(scope)?;
Ok(regext::arbitrarily_delete_subkey_all(
&key.parent_key,
regext::blank_path_guard(self.progid.to_string())?,
)?)
}
// YYC MARK:
// Reference: https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids#programmatic-identifier-elements-used-by-file-associations
// TODO:
// Currently we only support (Default), FriendlyTypeName and DefaultIcon
// to just cover the basic usage.
// We may expand these in future.
fn open_view_for_getter(&self, view: View) -> Result<RegKey> {
self.open_view_for_read(view)?
.this_key
.ok_or(Error::InexistantKey)
}
fn open_scope_for_setter(&self, scope: Scope) -> Result<RegKey> {
self.open_scope_for_write(scope)?
.this_key
.ok_or(Error::InexistantKey)
}
const NAMEOF_DEFAULT: &str = "";
pub fn get_default(&self, view: View) -> Result<Option<StrResVariant>> {
let key = self.open_view_for_getter(view)?;
// Get value of it
let value = regext::try_get_value::<String, _>(&key, Self::NAMEOF_DEFAULT)?;
// Transform it as result
Ok(value.map(|v| StrResVariant::from(v.as_str())))
}
///
///
/// The legacy way to set friendly name for this ProgId.
pub fn set_default(&mut self, scope: Scope, name: Option<&StrResVariant>) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
match name {
Some(name) => {
// Set value for this key
key.set_value(Self::NAMEOF_DEFAULT, &name.to_string())?;
}
None => {
// Delete this key
regext::arbitrarily_delete_value(&key, Self::NAMEOF_DEFAULT)?;
}
}
Ok(())
}
const NAMEOF_SHELL_VERB_PART1: &str = "shell";
const NAMEOF_SHELL_VERB_PART3: &str = "command";
const NAMEOF_SHELL_VERB_PART4: &str = "";
pub fn get_shell_verb(&self, view: View) -> Result<Option<ShellVerb>> {
let key = self.open_view_for_getter(view)?;
// Get shell subkey
let shell_key = match regext::try_open_subkey_with_flags(
&key,
Self::NAMEOF_SHELL_VERB_PART1,
PERM_R,
)? {
Some(key) => key,
None => return Ok(None),
};
// Get verb subkey name, then get subkey itself.
let verb_key_name = match regext::get_sole_subkey_name(&shell_key)? {
Some(key) => key,
None => return Ok(None),
};
let verb_key = match regext::try_open_subkey_with_flags(
&shell_key,
regext::blank_path_guard(&verb_key_name)?,
PERM_R,
)? {
Some(key) => key,
None => return Ok(None),
};
// Get command subkey.
let command_key = match regext::try_open_subkey_with_flags(
&verb_key,
Self::NAMEOF_SHELL_VERB_PART3,
PERM_R,
)? {
Some(key) => key,
None => return Ok(None),
};
// Get the default value of command subkey
let command_default_value = match regext::try_get_value::<String, _>(
&command_key,
Self::NAMEOF_SHELL_VERB_PART4,
)? {
Some(value) => value,
None => return Ok(None),
};
// Okey, return value.
Ok(Some(ShellVerb::new(
verb_key_name.parse()?,
command_default_value.parse()?,
)))
}
pub fn set_shell_verb(&mut self, scope: Scope, sv: Option<&ShellVerb>) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
match sv {
Some(sv) => {
// Create shell subkey
let (shell_key, _) =
key.create_subkey_with_flags(Self::NAMEOF_SHELL_VERB_PART1, PERM_RW)?;
// Create verb key
let (verb_key, _) =
shell_key.create_subkey_with_flags(sv.get_verb().inner(), PERM_RW)?;
// Create command key
let (command_key, _) =
verb_key.create_subkey_with_flags(Self::NAMEOF_SHELL_VERB_PART3, PERM_RW)?;
// Set command key default value
command_key.set_value(Self::NAMEOF_SHELL_VERB_PART4, &sv.get_command().full())?;
}
None => {
// Delete shell and its all subkey.
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_SHELL_VERB_PART1)?;
}
}
Ok(())
}
const NAMEOF_FRIENDLY_TYPE_NAME: &str = "FriendlyTypeName";
pub fn get_friendly_type_name(&self, view: View) -> Result<Option<StrResVariant>> {
let key = self.open_view_for_getter(view)?;
// Get value of it
let value = regext::try_get_value::<String, _>(&key, Self::NAMEOF_FRIENDLY_TYPE_NAME)?;
// Transform it as result
Ok(value.map(|v| StrResVariant::from(v.as_str())))
}
///
///
/// Set this entry to a friendly name for the ProgID.
pub fn set_friendly_type_name(
&mut self,
scope: Scope,
name: Option<&StrResVariant>,
) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
match name {
Some(name) => {
// Set value for this key
key.set_value(Self::NAMEOF_FRIENDLY_TYPE_NAME, &name.to_string())?;
}
None => {
// Delete this key
regext::arbitrarily_delete_value(&key, Self::NAMEOF_FRIENDLY_TYPE_NAME)?;
}
}
Ok(())
}
const NAMEOF_DEFAULT_ICON_PART1: &str = "DefaultIcon";
const NAMEOF_DEFAULT_ICON_PART2: &str = "";
pub fn get_default_icon(&self, view: View) -> Result<Option<IconResVariant>> {
let key = self.open_view_for_getter(view)?;
// Get default icon subkey
let default_icon_key = match regext::try_open_subkey_with_flags(
&key,
Self::NAMEOF_DEFAULT_ICON_PART1,
PERM_R,
)? {
Some(key) => key,
None => return Ok(None),
};
// Get the default value of default icon subkey
let default_icon_default_value =
regext::try_get_value::<String, _>(&default_icon_key, Self::NAMEOF_DEFAULT_ICON_PART2)?;
// Transform it as result
Ok(default_icon_default_value.map(|v| IconResVariant::from(v.as_str())))
}
pub fn set_default_icon(&mut self, scope: Scope, icon: Option<&IconResVariant>) -> Result<()> {
let key = self.open_scope_for_setter(scope)?;
match icon {
Some(icon) => {
// Create default icon subkey
let (default_icon_key, _) =
key.create_subkey_with_flags(Self::NAMEOF_DEFAULT_ICON_PART1, PERM_RW)?;
// Set default value of default icon subkey.
default_icon_key.set_value(Self::NAMEOF_DEFAULT_ICON_PART2, &icon.to_string())?;
}
None => {
// Delete shell and its all subkey.
regext::arbitrarily_delete_subkey_all(&key, Self::NAMEOF_DEFAULT_ICON_PART1)?;
}
}
Ok(())
}
}

View File

@@ -373,7 +373,7 @@ impl FromStr for IconRefStr {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
static RE: LazyLock<Regex> = LazyLock::new(|| { static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^([^,@].*),-([0-9]+)$").expect("unexpected bad regex pattern string") Regex::new(r"^([^,@][^,]*),-?([0-9]+)$").expect("unexpected bad regex pattern string")
}); });
let caps = RE.captures(s); let caps = RE.captures(s);
if let Some(caps) = caps { if let Some(caps) = caps {
@@ -461,7 +461,7 @@ impl FromStr for StrRefStr {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
static RE: LazyLock<Regex> = LazyLock::new(|| { static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^@(.+),-([0-9]+)$").expect("unexpected bad regex pattern string") Regex::new(r"^@([^,]+),-?([0-9]+)$").expect("unexpected bad regex pattern string")
}); });
let caps = RE.captures(s); let caps = RE.captures(s);
if let Some(caps) = caps { if let Some(caps) = caps {
@@ -510,6 +510,20 @@ 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.
/// ///
@@ -724,16 +738,16 @@ 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 usable string.
pub fn expand_string(&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;
// Fetch the size of expand result // Fetch the size of expand result
let source = WideCString::from_str(self.inner.as_str())?; let source = WideCString::from_str(self.inner.as_str())?;
// The return size is including null terminal.
let size = unsafe { ExpandEnvironmentStringsW(source.as_ptr(), std::ptr::null_mut(), 0) }; let size = unsafe { ExpandEnvironmentStringsW(source.as_ptr(), std::ptr::null_mut(), 0) };
if size == 0 { if size == 0 {
return Err(ExpandEnvVarError::ExpandFunction); return Err(ExpandEnvVarError::ExpandFunction);
} }
let size_no_nul = size.checked_sub(1).ok_or(ExpandEnvVarError::Underflow)?;
// Allocate buffer for it. // Allocate buffer for it.
let len: usize = size.try_into()?; let len: usize = size.try_into()?;
@@ -741,7 +755,7 @@ impl ExpandString {
let mut buffer = vec![0; len]; let mut buffer = vec![0; len];
// Receive result // Receive result
let size = let size =
unsafe { ExpandEnvironmentStringsW(source.as_ptr(), buffer.as_mut_ptr(), size_no_nul) }; unsafe { ExpandEnvironmentStringsW(source.as_ptr(), buffer.as_mut_ptr(), size) };
if size == 0 { if size == 0 {
return Err(ExpandEnvVarError::ExpandFunction); return Err(ExpandEnvVarError::ExpandFunction);
} }
@@ -867,16 +881,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")

View File

@@ -61,6 +61,54 @@ 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),
},
}
}
/// 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.
@@ -110,6 +158,9 @@ pub fn get_all_string_subkey_names(regkey: &RegKey) -> std::io::Result<Vec<Strin
/// 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("")
} }
@@ -135,6 +186,9 @@ impl BlankPathError {
/// Because it will cause unexpected behavior that returning key self, rather than subkey. /// Because it will cause unexpected behavior that returning key self, rather than subkey.
/// This is VERY dangerous especially for those registry delete functions. /// This is VERY dangerous especially for those registry delete functions.
/// So I create this function to prevent any harmful blank path was passed into registry function. /// So I create this function to prevent any harmful blank path was passed into registry function.
///
/// This function MUST be used for 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.
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())

27
wfassoc/tests/common.rs Normal file
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. ",
"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."
)
)
}

View File

@@ -1,4 +1,4 @@
use std::str::FromStr; use std::{collections::HashMap, str::FromStr};
use wfassoc::win32::concept::*; use wfassoc::win32::concept::*;
// region: File Extension // region: File Extension
@@ -134,6 +134,13 @@ fn test_icon_ref_str() {
r#"%SystemRoot%\System32\imageres.dll,-72"#, r#"%SystemRoot%\System32\imageres.dll,-72"#,
(r#"%SystemRoot%\System32\imageres.dll"#, 72), (r#"%SystemRoot%\System32\imageres.dll"#, 72),
); );
ok_tester(
r#""D:\Software\Krita\Krita (x64)\shellex\kritafile.ico",0"#,
(
r#""D:\Software\Krita\Krita (x64)\shellex\kritafile.ico""#,
0,
),
);
err_tester(r#"C:\Windows\Cursors\aero_arrow.cur"#); err_tester(r#"C:\Windows\Cursors\aero_arrow.cur"#);
err_tester(r#"This is my application, OK?"#); err_tester(r#"This is my application, OK?"#);
err_tester(r#"@%SystemRoot%\System32\shell32.dll,-30596"#); err_tester(r#"@%SystemRoot%\System32\shell32.dll,-30596"#);
@@ -161,6 +168,10 @@ fn test_str_ref_str() {
r#"@%SystemRoot%\System32\shell32.dll,-30596"#, r#"@%SystemRoot%\System32\shell32.dll,-30596"#,
(r#"%SystemRoot%\System32\shell32.dll"#, 30596), (r#"%SystemRoot%\System32\shell32.dll"#, 30596),
); );
ok_tester(
r#"@%SystemRoot%\System32\shell32.dll,30596"#,
(r#"%SystemRoot%\System32\shell32.dll"#, 30596),
);
err_tester(r#"This is my application, OK?"#); err_tester(r#"This is my application, OK?"#);
err_tester(r#"%SystemRoot%\System32\imageres.dll,-72"#); err_tester(r#"%SystemRoot%\System32\imageres.dll,-72"#);
} }
@@ -175,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())
@@ -184,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);
@@ -218,16 +235,36 @@ fn test_str_rc() {
#[test] #[test]
fn test_expand_string() { fn test_expand_string() {
fn tester(s: &str) { fn tester(fmt: &str, var_name: &str) {
let rv = ExpandString::new(s); // We first insert variable name into format string to get the final string
assert!(rv.is_ok()); let mut vars = HashMap::new();
let rv = rv.unwrap(); vars.insert("0".to_string(), var_name.to_string());
let final_string = strfmt::strfmt(fmt, &vars).unwrap();
let rv = rv.expand_string(); // The we try expanding final string first
assert!(rv.is_ok()); let expand_final_string = ExpandString::new(&final_string);
assert!(expand_final_string.is_ok());
let expand_final_string = expand_final_string.unwrap();
let expanded_final_string = expand_final_string.expand();
assert!(expanded_final_string.is_ok());
let expanded_final_string = expanded_final_string.unwrap();
// Then we expand variable name individually
let expand_var_name = ExpandString::new(var_name);
assert!(expand_var_name.is_ok());
let expand_var_name = expand_var_name.unwrap();
let expanded_var_name = expand_var_name.expand();
assert!(expanded_var_name.is_ok());
let expanded_var_name = expanded_var_name.unwrap();
// Finally, we directly insert expanded variable name into format string
// to get the string which can be compared with final string.
vars.insert("0".to_string(), expanded_var_name.clone());
let built_final_string = strfmt::strfmt(fmt, &vars).unwrap();
assert_eq!(expanded_final_string.to_string(), built_final_string);
} }
tester(r#"%SystemRoot%\System32\shell32.dll"#); tester(r#"{0}\System32\shell32.dll"#, "%SystemRoot%");
} }
// endregion // endregion

169
wfassoc/tests/highlevel.rs Normal file
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
wfassoc/tests/lowlevel.rs Normal file
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