@yufengtadian/freedom-cli 1.12.17 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/build.js +74 -9
- package/lib/cli.js +61 -3
- package/lib/init.js +9 -1
- package/lib/security.js +2 -2
- package/lib/utils.js +5 -2
- package/lib/verify.js +236 -0
- package/package.json +43 -39
- package/shell/darwin-arm64/freedom-shell +0 -0
- package/shell/linux-x64/freedom-shell +0 -0
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/templates/go/go.mod +1 -7
- package/templates/go/go.sum +2 -2
- package/templates/go/main.go +22 -8
- package/templates/go/pkg/freedom/anti_debug_other.go +2 -3
- package/templates/go/pkg/freedom/anti_debug_windows.go +10 -12
- package/templates/go/pkg/freedom/assets/{index.html → default.html} +1 -1
- package/templates/go/pkg/freedom/assets/freedom.js +219 -21
- package/templates/go/pkg/freedom/assets_embed.go +11 -13
- package/templates/go/pkg/freedom/authenticode_other.go +11 -0
- package/templates/go/pkg/freedom/authenticode_windows.go +87 -0
- package/templates/go/pkg/freedom/backend_proc.go +295 -51
- package/templates/go/pkg/freedom/bridge.go +13 -2
- package/templates/go/pkg/freedom/capability.go +77 -0
- package/templates/go/pkg/freedom/center_other.go +3 -0
- package/templates/go/pkg/freedom/center_windows.go +64 -36
- package/templates/go/pkg/freedom/dispatch.go +60 -0
- package/templates/go/pkg/freedom/events_windows.go +319 -0
- package/templates/go/pkg/freedom/freedom.go +242 -92
- package/templates/go/pkg/freedom/msgwindow_windows.go +257 -0
- package/templates/go/pkg/freedom/osver_other.go +15 -0
- package/templates/go/pkg/freedom/osver_windows.go +121 -0
- package/templates/go/pkg/freedom/proc_hide_windows.go +2 -2
- package/templates/go/pkg/freedom/{configfile.go → resources.go} +56 -32
- package/templates/go/pkg/freedom/security.go +19 -12
- package/templates/go/pkg/freedom/singleinstance_other.go +10 -0
- package/templates/go/pkg/freedom/singleinstance_windows.go +145 -0
- package/templates/go/pkg/freedom/spawn_other.go +11 -0
- package/templates/go/pkg/freedom/spawn_windows.go +23 -0
- package/templates/go/pkg/freedom/store.go +458 -0
- package/templates/go/pkg/freedom/syscap_linux.go +278 -0
- package/templates/go/pkg/freedom/syscap_other.go +22 -0
- package/templates/go/pkg/freedom/syscap_windows.go +845 -0
- package/templates/go/pkg/freedom/sysint_common.go +70 -0
- package/templates/go/pkg/freedom/sysint_windows.go +426 -0
- package/templates/go/pkg/freedom/tray_common.go +12 -0
- package/templates/go/pkg/freedom/tray_linux.go +302 -0
- package/templates/go/pkg/freedom/tray_other.go +10 -0
- package/templates/go/pkg/freedom/tray_windows.go +536 -0
- package/templates/go/pkg/freedom/updater.go +372 -0
- package/templates/go/pkg/freedom/window_mgr.go +385 -0
- package/templates/go/pkg/freedom/window_other.go +32 -66
- package/templates/go/pkg/freedom/window_windows.go +406 -251
- package/templates/project-minimal/freedom.config.js +58 -0
- package/templates/project-minimal/index.html +41 -0
- package/templates/project-minimal/package.json +14 -0
- package/templates/project-minimal/src/main.js +19 -0
- package/templates/project-minimal/vite.config.js +18 -0
- package/templates/go/gen_config.go +0 -22
- package/templates/go/pkg/freedom/security_test.go +0 -153
- package/templates/go/webview_go/.github/workflows/ci.yaml +0 -62
- package/templates/go/webview_go/CHANGELOG.md +0 -15
- package/templates/go/webview_go/LICENSE +0 -22
- package/templates/go/webview_go/README.md +0 -50
- package/templates/go/webview_go/examples/basic/main.go +0 -12
- package/templates/go/webview_go/examples/bind/main.go +0 -38
- package/templates/go/webview_go/glue.c +0 -36
- package/templates/go/webview_go/go.mod +0 -3
- package/templates/go/webview_go/go.sum +0 -0
- package/templates/go/webview_go/libs/mswebview2/LICENSE +0 -27
- package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +0 -23568
- package/templates/go/webview_go/libs/mswebview2/include/vendor.go +0 -2
- package/templates/go/webview_go/libs/mswebview2/vendor.go +0 -2
- package/templates/go/webview_go/libs/mswebview2/version.txt +0 -1
- package/templates/go/webview_go/libs/webview/LICENSE +0 -22
- package/templates/go/webview_go/libs/webview/include/vendor.go +0 -2
- package/templates/go/webview_go/libs/webview/include/webview.h +0 -3871
- package/templates/go/webview_go/libs/webview/vendor.go +0 -2
- package/templates/go/webview_go/libs/webview/version.txt +0 -1
- package/templates/go/webview_go/webview.cc +0 -1
- package/templates/go/webview_go/webview.go +0 -481
- package/templates/go/webview_go/webview_test.go +0 -50
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
package freedom
|
|
2
|
+
|
|
3
|
+
// 跨平台纯函数助手(M4 起):Windows/Linux 两侧分发器共用的校验与解析,
|
|
4
|
+
// 无平台依赖,从 *_windows.go 抽出以单一实现(原 windows 侧引用不变)。
|
|
5
|
+
|
|
6
|
+
import (
|
|
7
|
+
"encoding/base64"
|
|
8
|
+
"encoding/json"
|
|
9
|
+
"os"
|
|
10
|
+
"strings"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// shellTargetAllowed 校验 openExternal 目标:仅放行 http/https/mailto URL。
|
|
14
|
+
// 前端传入不可全信,白名单之外的 scheme 与本地路径一律拒绝
|
|
15
|
+
// (Windows 侧 ShellExecuteW 可执行任意合法字符串;Linux 侧 xdg-open 同理)。
|
|
16
|
+
func shellTargetAllowed(target string) bool {
|
|
17
|
+
lower := strings.ToLower(target)
|
|
18
|
+
return strings.HasPrefix(lower, "http://") ||
|
|
19
|
+
strings.HasPrefix(lower, "https://") ||
|
|
20
|
+
strings.HasPrefix(lower, "mailto:")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// reservedSchemes 是系统/浏览器保留协议:注册它们会劫持网页链接或 shell 行为,一律拒绝。
|
|
24
|
+
var reservedSchemes = map[string]bool{
|
|
25
|
+
"http": true, "https": true, "file": true, "ftp": true, "mailto": true,
|
|
26
|
+
"shell": true, "search-ms": true, "javascript": true, "data": true,
|
|
27
|
+
"about": true, "resource": true, "res": true, "mhtml": true, "ms-appx": true,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// protocolSchemeAllowed 判定 scheme 是否允许注册(保留名单 + ms-/microsoft. 前缀排除)。
|
|
31
|
+
func protocolSchemeAllowed(scheme string) bool {
|
|
32
|
+
lower := strings.ToLower(scheme)
|
|
33
|
+
if reservedSchemes[lower] {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
return !strings.HasPrefix(lower, "ms-") && !strings.HasPrefix(lower, "microsoft.")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func validScheme(s string) bool {
|
|
40
|
+
if s == "" || !((s[0] >= 'a' && s[0] <= 'z') || (s[0] >= 'A' && s[0] <= 'Z')) {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
for i := 1; i < len(s); i++ {
|
|
44
|
+
c := s[i]
|
|
45
|
+
if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' ||
|
|
46
|
+
c == '+' || c == '-' || c == '.') {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// launchArgsJSON 返回进程启动参数(不含 exe 自身),供前端解析 deep link。
|
|
54
|
+
func launchArgsJSON() json.RawMessage {
|
|
55
|
+
b, _ := json.Marshal(os.Args[1:])
|
|
56
|
+
return b
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// dataURLToBytes 解析 "data:...;base64,XXXX" 为原始字节。
|
|
60
|
+
func dataURLToBytes(dataURL string) []byte {
|
|
61
|
+
idx := strings.Index(dataURL, "base64,")
|
|
62
|
+
if idx < 0 {
|
|
63
|
+
return nil
|
|
64
|
+
}
|
|
65
|
+
b, err := base64.StdEncoding.DecodeString(dataURL[idx+len("base64,"):])
|
|
66
|
+
if err != nil {
|
|
67
|
+
return nil
|
|
68
|
+
}
|
|
69
|
+
return b
|
|
70
|
+
}
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
//go:build windows
|
|
2
|
+
|
|
3
|
+
package freedom
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"fmt"
|
|
7
|
+
"os"
|
|
8
|
+
"os/exec"
|
|
9
|
+
"strings"
|
|
10
|
+
"syscall"
|
|
11
|
+
"unsafe"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// W2 系统集成(对标 Tauri 官方插件集):
|
|
15
|
+
// - 热键组合解析(global-shortcut)
|
|
16
|
+
// - 剪贴板读写(clipboard-manager)
|
|
17
|
+
// - 系统通知 Toast(notification)
|
|
18
|
+
// - openExternal(opener/shell.open)
|
|
19
|
+
// - 开机自启(autostart,HKCU Run 键)
|
|
20
|
+
// - URL Scheme 注册(deep-link,HKCU Classes)
|
|
21
|
+
// 前端入口统一走 __freedom_sys(sysCapCall 分发)。
|
|
22
|
+
|
|
23
|
+
// ---- 热键 ----
|
|
24
|
+
|
|
25
|
+
const (
|
|
26
|
+
modAlt = 0x0001
|
|
27
|
+
modControl = 0x0002
|
|
28
|
+
modShift = 0x0004
|
|
29
|
+
modWin = 0x0008
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
var (
|
|
33
|
+
hotkeyModifiers = map[string]uint32{
|
|
34
|
+
"alt": modAlt, "option": modAlt,
|
|
35
|
+
"ctrl": modControl, "control": modControl,
|
|
36
|
+
"shift": modShift,
|
|
37
|
+
"win": modWin, "cmd": modWin, "meta": modWin, "super": modWin,
|
|
38
|
+
}
|
|
39
|
+
hotkeyVKs = map[string]uint32{}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
func init() {
|
|
43
|
+
for c := 'A'; c <= 'Z'; c++ {
|
|
44
|
+
hotkeyVKs[string(rune(c))] = uint32(c) // VK_A..VK_Z 与 ASCII 大写字母同值
|
|
45
|
+
}
|
|
46
|
+
for d := '0'; d <= '9'; d++ {
|
|
47
|
+
hotkeyVKs[string(d)] = uint32(d) // VK_0..VK_9 与 ASCII 数字同值
|
|
48
|
+
}
|
|
49
|
+
for i := 1; i <= 24; i++ {
|
|
50
|
+
hotkeyVKs[fmt.Sprintf("F%d", i)] = uint32(0x70 + i - 1)
|
|
51
|
+
}
|
|
52
|
+
extra := map[string]uint32{
|
|
53
|
+
"SPACE": 0x20, "ENTER": 0x0D, "RETURN": 0x0D, "TAB": 0x09,
|
|
54
|
+
"ESC": 0x1B, "ESCAPE": 0x1B, "BACKSPACE": 0x08, "DELETE": 0x2E, "DEL": 0x2E,
|
|
55
|
+
"INSERT": 0x2D, "HOME": 0x24, "END": 0x23, "PAGEUP": 0x21, "PRIOR": 0x21,
|
|
56
|
+
"PAGEDOWN": 0x22, "NEXT": 0x22, "LEFT": 0x25, "UP": 0x26, "RIGHT": 0x27, "DOWN": 0x28,
|
|
57
|
+
"PRINTSCREEN": 0x2C, "SNAPSHOT": 0x2C,
|
|
58
|
+
}
|
|
59
|
+
for k, v := range extra {
|
|
60
|
+
hotkeyVKs[k] = v
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// parseHotkey 解析 "ctrl+alt+k" 风格组合 → (modifiers, vk)。
|
|
65
|
+
func parseHotkey(combo string) (uint32, uint32, error) {
|
|
66
|
+
parts := strings.Split(combo, "+")
|
|
67
|
+
key := strings.ToUpper(strings.TrimSpace(parts[len(parts)-1]))
|
|
68
|
+
var mods uint32
|
|
69
|
+
for _, m := range parts[:len(parts)-1] {
|
|
70
|
+
flag, ok := hotkeyModifiers[strings.ToLower(strings.TrimSpace(m))]
|
|
71
|
+
if !ok {
|
|
72
|
+
return 0, 0, fmt.Errorf("未知修饰键 %q", m)
|
|
73
|
+
}
|
|
74
|
+
mods |= flag
|
|
75
|
+
}
|
|
76
|
+
vk, ok := hotkeyVKs[key]
|
|
77
|
+
if !ok {
|
|
78
|
+
return 0, 0, fmt.Errorf("未知按键 %q", key)
|
|
79
|
+
}
|
|
80
|
+
return mods, vk, nil
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---- 剪贴板 ----
|
|
84
|
+
|
|
85
|
+
const (
|
|
86
|
+
cfText = 1
|
|
87
|
+
cfUnicodeText = 13
|
|
88
|
+
gmemMoveable = 0x0002
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
// clipboardReadText 读取系统剪贴板文本(优先 CF_UNICODETEXT,回退 CF_TEXT)。
|
|
92
|
+
func clipboardReadText() (string, error) {
|
|
93
|
+
if r, _, e := procOpenClipboard.Call(0); r == 0 {
|
|
94
|
+
return "", fmt.Errorf("freedom: OpenClipboard: %w", e)
|
|
95
|
+
}
|
|
96
|
+
defer procCloseClipboard.Call()
|
|
97
|
+
for _, fmtID := range []uintptr{cfUnicodeText, cfText} {
|
|
98
|
+
av, _, _ := procIsClipboardFormatAvai.Call(fmtID)
|
|
99
|
+
if av == 0 {
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
h, _, _ := procGetClipboardData.Call(fmtID)
|
|
103
|
+
if h == 0 {
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
p, _, _ := procGlobalLock.Call(h)
|
|
107
|
+
if p == 0 {
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
var s string
|
|
111
|
+
size, _, _ := procGlobalSize.Call(h)
|
|
112
|
+
if size == 0 {
|
|
113
|
+
procGlobalUnlock.Call(h)
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
if fmtID == cfUnicodeText {
|
|
117
|
+
u16 := unsafe.Slice((*uint16)(unsafe.Pointer(p)), size/2)
|
|
118
|
+
s = syscall.UTF16ToString(u16)
|
|
119
|
+
} else {
|
|
120
|
+
b := unsafe.Slice((*byte)(unsafe.Pointer(p)), size)
|
|
121
|
+
n := 0
|
|
122
|
+
for n < len(b) && b[n] != 0 {
|
|
123
|
+
n++
|
|
124
|
+
}
|
|
125
|
+
s = string(b[:n])
|
|
126
|
+
}
|
|
127
|
+
procGlobalUnlock.Call(h)
|
|
128
|
+
return s, nil
|
|
129
|
+
}
|
|
130
|
+
return "", nil
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// clipboardWriteText 写入系统剪贴板文本。
|
|
134
|
+
// ownerHwnd 为剪贴板属主窗口(OpenClipboard 要求有效句柄,传 0 会静默失去属主语义)。
|
|
135
|
+
func clipboardWriteText(text string, ownerHwnd uintptr) error {
|
|
136
|
+
if ownerHwnd == 0 {
|
|
137
|
+
return fmt.Errorf("freedom: 剪贴板写入缺少属主窗口句柄")
|
|
138
|
+
}
|
|
139
|
+
u16, err := syscall.UTF16FromString(text)
|
|
140
|
+
if err != nil {
|
|
141
|
+
return fmt.Errorf("freedom: 剪贴板文本含 NUL: %w", err)
|
|
142
|
+
}
|
|
143
|
+
if r, _, e := procOpenClipboard.Call(ownerHwnd); r == 0 {
|
|
144
|
+
return fmt.Errorf("freedom: OpenClipboard: %w", e)
|
|
145
|
+
}
|
|
146
|
+
defer procCloseClipboard.Call()
|
|
147
|
+
if r, _, e := procEmptyClipboard.Call(); r == 0 {
|
|
148
|
+
return fmt.Errorf("freedom: EmptyClipboard: %w", e)
|
|
149
|
+
}
|
|
150
|
+
size := uintptr(len(u16) * 2)
|
|
151
|
+
h, _, e := procGlobalAlloc.Call(gmemMoveable, size)
|
|
152
|
+
if h == 0 {
|
|
153
|
+
return fmt.Errorf("freedom: GlobalAlloc: %w", e)
|
|
154
|
+
}
|
|
155
|
+
p, _, _ := procGlobalLock.Call(h)
|
|
156
|
+
if p == 0 {
|
|
157
|
+
procGlobalFree.Call(h)
|
|
158
|
+
return fmt.Errorf("freedom: GlobalLock 失败")
|
|
159
|
+
}
|
|
160
|
+
copy(unsafe.Slice((*uint16)(unsafe.Pointer(p)), len(u16)), u16)
|
|
161
|
+
procGlobalUnlock.Call(h)
|
|
162
|
+
// SetClipboardData 成功后系统接管内存所有权,不得再 GlobalFree。
|
|
163
|
+
if r, _, e := procSetClipboardData.Call(cfUnicodeText, h); r == 0 {
|
|
164
|
+
procGlobalFree.Call(h)
|
|
165
|
+
return fmt.Errorf("freedom: SetClipboardData: %w", e)
|
|
166
|
+
}
|
|
167
|
+
return nil
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---- 系统通知(Toast via PowerShell WinRT 投影)----
|
|
171
|
+
|
|
172
|
+
// xmlEscape 转义 XML 文本节点内容(通知标题/正文注入面)。
|
|
173
|
+
func xmlEscape(s string) string {
|
|
174
|
+
r := strings.NewReplacer("&", "&", "<", "<", ">", ">", `"`, """, "'", "'")
|
|
175
|
+
return r.Replace(s)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// psSingleQuote 把字符串转义为 PowerShell 单引号字面量。
|
|
179
|
+
func psSingleQuote(s string) string { return "'" + strings.ReplaceAll(s, "'", "''") + "'" }
|
|
180
|
+
|
|
181
|
+
func showToast(title, body string) error {
|
|
182
|
+
toastXML := fmt.Sprintf(
|
|
183
|
+
`<toast><visual><binding template="ToastGeneric"><text>%s</text><text>%s</text></binding></visual></toast>`,
|
|
184
|
+
xmlEscape(title), xmlEscape(body))
|
|
185
|
+
// Windows.UI.Notifications 经 PowerShell 投影;AppId 用宿主 exe 名。
|
|
186
|
+
appID := "Freedom.App"
|
|
187
|
+
script := fmt.Sprintf(`[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null; `+
|
|
188
|
+
`$xml = [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime]::new(); `+
|
|
189
|
+
`$xml.LoadXml(%s); `+
|
|
190
|
+
`$t = [Windows.UI.Notifications.ToastNotification]::new($xml); `+
|
|
191
|
+
`[Windows.UI.Notifications.ToastNotificationManager]::GetToastNotifier(%s).Show($t)`,
|
|
192
|
+
psSingleQuote(toastXML), psSingleQuote(appID))
|
|
193
|
+
cmd := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", script)
|
|
194
|
+
hideWindow(cmd)
|
|
195
|
+
if out, err := cmd.CombinedOutput(); err != nil {
|
|
196
|
+
return fmt.Errorf("freedom: 通知发送失败: %w (%s)", err, strings.TrimSpace(string(out)))
|
|
197
|
+
}
|
|
198
|
+
return nil
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---- openExternal ----
|
|
202
|
+
|
|
203
|
+
// shellOpen 用系统默认关联程序打开 URL(白名单见 shellTargetAllowed,SW_SHOWNORMAL=1)。
|
|
204
|
+
func shellOpen(target string) error {
|
|
205
|
+
if target == "" {
|
|
206
|
+
return fmt.Errorf("freedom: shell.open 缺少 target")
|
|
207
|
+
}
|
|
208
|
+
if !shellTargetAllowed(target) {
|
|
209
|
+
return fmt.Errorf("freedom: 拒绝打开非白名单目标 %q(仅允许 http/https/mailto)", target)
|
|
210
|
+
}
|
|
211
|
+
p, err := syscall.UTF16PtrFromString(target)
|
|
212
|
+
if err != nil {
|
|
213
|
+
return fmt.Errorf("freedom: target 含 NUL")
|
|
214
|
+
}
|
|
215
|
+
r, _, e := procShellExecuteW.Call(0,
|
|
216
|
+
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("open"))),
|
|
217
|
+
uintptr(unsafe.Pointer(p)), 0, 0, 1)
|
|
218
|
+
if int64(uintptr(r)) <= 32 {
|
|
219
|
+
return fmt.Errorf("freedom: ShellExecuteW(%s) 失败: code=%d (%w)", target, int64(int32(uint32(r))), e)
|
|
220
|
+
}
|
|
221
|
+
return nil
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---- 开机自启(HKCU ...\Run)----
|
|
225
|
+
|
|
226
|
+
const (
|
|
227
|
+
hkeyCurrentUser = uintptr(0x80000001)
|
|
228
|
+
// KEY_SET_VALUE(0x2)|KEY_QUERY_VALUE(0x1)|KEY_WOW64_64KEY(0x100)
|
|
229
|
+
keyWrite = 0x103
|
|
230
|
+
keyRead = 0x101
|
|
231
|
+
regSz = 1
|
|
232
|
+
|
|
233
|
+
runKeyPath = `Software\Microsoft\Windows\CurrentVersion\Run`
|
|
234
|
+
classesBase = `Software\Classes`
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
func regOpen(base uintptr, path string, access uint32) (uintptr, error) {
|
|
238
|
+
p, err := syscall.UTF16PtrFromString(path)
|
|
239
|
+
if err != nil {
|
|
240
|
+
return 0, err
|
|
241
|
+
}
|
|
242
|
+
var hk uintptr
|
|
243
|
+
if hr, _, e := procRegOpenKeyExW.Call(base, uintptr(unsafe.Pointer(p)), 0, uintptr(access),
|
|
244
|
+
uintptr(unsafe.Pointer(&hk))); hr != 0 {
|
|
245
|
+
return 0, fmt.Errorf("RegOpenKeyExW(%s): errno=%d (%w)", path, hr, e)
|
|
246
|
+
}
|
|
247
|
+
return hk, nil
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
func regSetSz(hk uintptr, name, value string) error {
|
|
251
|
+
np, err := syscall.UTF16PtrFromString(name)
|
|
252
|
+
if err != nil {
|
|
253
|
+
return err
|
|
254
|
+
}
|
|
255
|
+
vb, err := syscall.UTF16FromString(value)
|
|
256
|
+
if err != nil {
|
|
257
|
+
return err
|
|
258
|
+
}
|
|
259
|
+
hr, _, e := procRegSetValueExW.Call(hk, uintptr(unsafe.Pointer(np)), 0, regSz,
|
|
260
|
+
uintptr(unsafe.Pointer(&vb[0])), uintptr(len(vb)*2))
|
|
261
|
+
if hr != 0 {
|
|
262
|
+
return fmt.Errorf("RegSetValueExW(%s): errno=%d (%w)", name, hr, e)
|
|
263
|
+
}
|
|
264
|
+
return nil
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
func regQuerySz(hk uintptr, name string) (string, bool, error) {
|
|
268
|
+
np, err := syscall.UTF16PtrFromString(name)
|
|
269
|
+
if err != nil {
|
|
270
|
+
return "", false, err
|
|
271
|
+
}
|
|
272
|
+
const max = 4096
|
|
273
|
+
buf := make([]uint16, max)
|
|
274
|
+
var sz uint32 = max * 2
|
|
275
|
+
var kind uint32
|
|
276
|
+
hr, _, e := procRegQueryValueExW.Call(hk, uintptr(unsafe.Pointer(np)), 0,
|
|
277
|
+
uintptr(unsafe.Pointer(&kind)), uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&sz)))
|
|
278
|
+
if hr == 2 { // ERROR_FILE_NOT_FOUND
|
|
279
|
+
return "", false, nil
|
|
280
|
+
}
|
|
281
|
+
if hr != 0 {
|
|
282
|
+
return "", false, fmt.Errorf("RegQueryValueExW(%s): errno=%d (%w)", name, hr, e)
|
|
283
|
+
}
|
|
284
|
+
return syscall.UTF16ToString(buf), true, nil
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// setAutostart 写入/删除 HKCU Run 启动项;args 为附加启动参数。
|
|
288
|
+
// 名称经 sanitizeName 收敛;删除/覆盖前先做属主校验——现值指向非本程序时拒绝,
|
|
289
|
+
// 防止前端传入任意 name 破坏其他软件的启动项。
|
|
290
|
+
func setAutostart(name string, enabled bool, args string) error {
|
|
291
|
+
name = sanitizeName(name)
|
|
292
|
+
if name == "" {
|
|
293
|
+
return fmt.Errorf("freedom: 自启项名称为空或非法")
|
|
294
|
+
}
|
|
295
|
+
hk, err := regOpen(hkeyCurrentUser, runKeyPath, keyWrite)
|
|
296
|
+
if err != nil {
|
|
297
|
+
return err
|
|
298
|
+
}
|
|
299
|
+
defer procRegCloseKey.Call(hk)
|
|
300
|
+
exePath, err := os.Executable()
|
|
301
|
+
if err != nil {
|
|
302
|
+
return fmt.Errorf("freedom: 无法定位自身路径: %w", err)
|
|
303
|
+
}
|
|
304
|
+
cur, found, err := regQuerySz(hk, name)
|
|
305
|
+
if err != nil {
|
|
306
|
+
return err
|
|
307
|
+
}
|
|
308
|
+
if found && !valueOwnedBySelf(cur, exePath) {
|
|
309
|
+
return fmt.Errorf("freedom: 启动项 %q 已存在且不属于本程序(值为 %q),拒绝覆盖", name, cur)
|
|
310
|
+
}
|
|
311
|
+
if !enabled {
|
|
312
|
+
if !found {
|
|
313
|
+
return nil
|
|
314
|
+
}
|
|
315
|
+
np, err := syscall.UTF16PtrFromString(name)
|
|
316
|
+
if err != nil {
|
|
317
|
+
return err
|
|
318
|
+
}
|
|
319
|
+
if hr, _, _ := procRegDeleteValueW.Call(hk, uintptr(unsafe.Pointer(np))); hr != 0 && hr != 2 {
|
|
320
|
+
return fmt.Errorf("RegDeleteValueW(%s): errno=%d", name, hr)
|
|
321
|
+
}
|
|
322
|
+
return nil
|
|
323
|
+
}
|
|
324
|
+
value := `"` + exePath + `"`
|
|
325
|
+
if a := strings.TrimSpace(args); a != "" {
|
|
326
|
+
value += " " + a
|
|
327
|
+
}
|
|
328
|
+
return regSetSz(hk, name, value)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// valueOwnedBySelf 判断 Run 键值是否指向本 exe(大小写不敏感前缀匹配引号内路径)。
|
|
332
|
+
func valueOwnedBySelf(value, exePath string) bool {
|
|
333
|
+
v := strings.ToLower(strings.TrimSpace(value))
|
|
334
|
+
p := strings.ToLower(exePath)
|
|
335
|
+
return strings.HasPrefix(v, `"`+p+`"`) || strings.HasPrefix(v, p)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
func getAutostart(name string) (bool, error) {
|
|
339
|
+
name = sanitizeName(name)
|
|
340
|
+
if name == "" {
|
|
341
|
+
return false, fmt.Errorf("freedom: 自启项名称为空或非法")
|
|
342
|
+
}
|
|
343
|
+
hk, err := regOpen(hkeyCurrentUser, runKeyPath, keyRead)
|
|
344
|
+
if err != nil {
|
|
345
|
+
return false, err
|
|
346
|
+
}
|
|
347
|
+
defer procRegCloseKey.Call(hk)
|
|
348
|
+
_, found, err := regQuerySz(hk, name)
|
|
349
|
+
return found, err
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ---- URL Scheme(deep link 注册表侧)----
|
|
353
|
+
|
|
354
|
+
// registerProtocol 在 HKCU\Software\Classes 下注册 URL Scheme:
|
|
355
|
+
// 之后系统内任意处打开 "<scheme>:..." 都会带参数拉起本 exe。
|
|
356
|
+
func registerProtocol(scheme, displayName string) error {
|
|
357
|
+
if scheme == "" || !validScheme(scheme) {
|
|
358
|
+
return fmt.Errorf("freedom: 非法 scheme %q(需匹配 ^[a-zA-Z][a-zA-Z0-9+.-]*$)", scheme)
|
|
359
|
+
}
|
|
360
|
+
if !protocolSchemeAllowed(scheme) {
|
|
361
|
+
return fmt.Errorf("freedom: 拒绝注册保留 scheme %q", scheme)
|
|
362
|
+
}
|
|
363
|
+
exePath, err := os.Executable()
|
|
364
|
+
if err != nil {
|
|
365
|
+
return fmt.Errorf("freedom: 无法定位自身路径: %w", err)
|
|
366
|
+
}
|
|
367
|
+
base := classesBase + `\` + scheme
|
|
368
|
+
// HKCU\Software 以 KEY_WRITE 打开后逐级 RegCreateKeyEx 更稳,
|
|
369
|
+
// 这里用 RegCreateKeyExW(advapi32)保证多级自动创建。
|
|
370
|
+
root, err := regCreate(hkeyCurrentUser, base)
|
|
371
|
+
if err != nil {
|
|
372
|
+
return err
|
|
373
|
+
}
|
|
374
|
+
defer procRegCloseKey.Call(root)
|
|
375
|
+
if displayName == "" {
|
|
376
|
+
displayName = "URL:" + scheme
|
|
377
|
+
}
|
|
378
|
+
if err := regSetSz(root, "", displayName); err != nil {
|
|
379
|
+
return err
|
|
380
|
+
}
|
|
381
|
+
if err := regSetSz(root, "URL Protocol", ""); err != nil {
|
|
382
|
+
return err
|
|
383
|
+
}
|
|
384
|
+
cmdKey, err := regCreate(root, `shell\open\command`)
|
|
385
|
+
if err != nil {
|
|
386
|
+
return err
|
|
387
|
+
}
|
|
388
|
+
defer procRegCloseKey.Call(cmdKey)
|
|
389
|
+
return regSetSz(cmdKey, "", `"`+exePath+`" "%1"`)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
func unregisterProtocol(scheme string) error {
|
|
393
|
+
if scheme == "" || !validScheme(scheme) {
|
|
394
|
+
return fmt.Errorf("freedom: 非法 scheme %q", scheme)
|
|
395
|
+
}
|
|
396
|
+
if !protocolSchemeAllowed(scheme) {
|
|
397
|
+
return fmt.Errorf("freedom: 拒绝删除保留 scheme %q", scheme)
|
|
398
|
+
}
|
|
399
|
+
path := classesBase + `\` + scheme
|
|
400
|
+
sp, err := syscall.UTF16PtrFromString(path)
|
|
401
|
+
if err != nil {
|
|
402
|
+
return err
|
|
403
|
+
}
|
|
404
|
+
// SHDeleteKeyW 递归删除整棵子树(advapi 无递归删除,Win7 前 API)。
|
|
405
|
+
hr, _, e := procSHDeleteKeyW.Call(hkeyCurrentUser, uintptr(unsafe.Pointer(sp)))
|
|
406
|
+
if hr != 0 {
|
|
407
|
+
return fmt.Errorf("SHDeleteKeyW(%s): errno=%d (%w)", scheme, int64(int32(uint32(hr))), e)
|
|
408
|
+
}
|
|
409
|
+
return nil
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// regCreate 逐级创建(打开已存在的键)并返回句柄。
|
|
413
|
+
func regCreate(base uintptr, path string) (uintptr, error) {
|
|
414
|
+
p, err := syscall.UTF16PtrFromString(path)
|
|
415
|
+
if err != nil {
|
|
416
|
+
return 0, err
|
|
417
|
+
}
|
|
418
|
+
var hk uintptr
|
|
419
|
+
var disp uint32
|
|
420
|
+
hr, _, e := procRegCreateKeyExW.Call(base, uintptr(unsafe.Pointer(p)), 0, 0, 0,
|
|
421
|
+
keyWrite, 0, uintptr(unsafe.Pointer(&hk)), uintptr(unsafe.Pointer(&disp)))
|
|
422
|
+
if hr != 0 {
|
|
423
|
+
return 0, fmt.Errorf("RegCreateKeyExW(%s): errno=%d (%w)", path, hr, e)
|
|
424
|
+
}
|
|
425
|
+
return hk, nil
|
|
426
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
package freedom
|
|
2
|
+
|
|
3
|
+
// menuItem 描述一个托盘/菜单栏条目(JSON 透传)。M4 起为跨平台公共结构
|
|
4
|
+
// (Windows 托盘/菜单栏与 Linux GTK 托盘共用;原声明在 tray_windows.go)。
|
|
5
|
+
type menuItem struct {
|
|
6
|
+
ID string `json:"id"`
|
|
7
|
+
Label string `json:"label"`
|
|
8
|
+
Type string `json:"type"` // "item"(默认)/ "separator" / "submenu"
|
|
9
|
+
Enabled *bool `json:"enabled"` // nil=默认启用
|
|
10
|
+
Checked *bool `json:"checked"`
|
|
11
|
+
Submenu []menuItem `json:"submenu"`
|
|
12
|
+
}
|