@yufengtadian/freedom-cli 1.12.16 → 1.12.18

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.
Files changed (37) hide show
  1. package/README.md +38 -3
  2. package/lib/build.js +132 -12
  3. package/lib/cli.js +94 -4
  4. package/lib/config.js +12 -1
  5. package/lib/init.js +9 -1
  6. package/lib/security.js +150 -0
  7. package/lib/utils.js +5 -2
  8. package/lib/verify.js +236 -0
  9. package/package.json +39 -39
  10. package/shell/win-x64/freedom-shell.exe +0 -0
  11. package/templates/go/build_all.txt +0 -0
  12. package/templates/go/build_err.txt +0 -0
  13. package/templates/go/pkg/freedom/anti_debug_other.go +8 -0
  14. package/templates/go/pkg/freedom/anti_debug_windows.go +39 -0
  15. package/templates/go/pkg/freedom/assets/freedom.js +19 -16
  16. package/templates/go/pkg/freedom/backend_proc.go +7 -1
  17. package/templates/go/pkg/freedom/center_windows.go +36 -18
  18. package/templates/go/pkg/freedom/center_windows_test.go +32 -0
  19. package/templates/go/pkg/freedom/configfile.go +56 -7
  20. package/templates/go/pkg/freedom/freedom.go +70 -8
  21. package/templates/go/pkg/freedom/security.go +227 -0
  22. package/templates/go/pkg/freedom/security_test.go +153 -0
  23. package/templates/go/pkg/freedom/syscap_other.go +10 -0
  24. package/templates/go/pkg/freedom/syscap_windows.go +653 -0
  25. package/templates/go/pkg/freedom/tray_other.go +10 -0
  26. package/templates/go/pkg/freedom/tray_windows.go +506 -0
  27. package/templates/go/pkg/freedom/window_windows.go +28 -16
  28. package/templates/go/temp_vet_test/main.go +40 -0
  29. package/templates/go/test_err.txt +0 -0
  30. package/templates/go/vet_err.txt +1 -0
  31. package/templates/go/vet_err2.txt +2 -0
  32. package/templates/project/freedom.config.js +8 -0
  33. package/templates/project-minimal/freedom.config.js +58 -0
  34. package/templates/project-minimal/index.html +41 -0
  35. package/templates/project-minimal/package.json +14 -0
  36. package/templates/project-minimal/src/main.js +19 -0
  37. package/templates/project-minimal/vite.config.js +18 -0
@@ -0,0 +1,506 @@
1
+ //go:build windows
2
+
3
+ package freedom
4
+
5
+ // 系统托盘(Shell_NotifyIcon)与原生菜单栏(HMENU)。
6
+ //
7
+ // 托盘通过一个隐藏消息窗口接收回调消息(WM_APP+1),菜单项点击经 WM_COMMAND
8
+ // 分发,统一通过 App.Emit 推送到前端(freedom.tray.on('click'/'menu', ...))。
9
+ // 原生菜单栏通过 SetMenu 挂到主窗口,同样经 WM_COMMAND 回调。
10
+ //
11
+ // 与主窗口同一 UI 线程创建,消息由同一消息泵派发,无需额外线程。
12
+
13
+ import (
14
+ "encoding/json"
15
+ "fmt"
16
+ "sync"
17
+ "syscall"
18
+ "unsafe"
19
+ )
20
+
21
+ // ---- 常量 ----
22
+
23
+ const (
24
+ wmApp = 0x8000 // WM_APP
25
+ wmCommand = 0x0111
26
+ wmLButtonUp = 0x0202
27
+ wmLButtonDbl = 0x0203
28
+ wmRButtonUp = 0x0205
29
+ wmNull = 0x0000
30
+
31
+ nimAdd = 0x00000000
32
+ nimModify = 0x00000001
33
+ nimDelete = 0x00000002
34
+
35
+ nifMessage = 0x00000001
36
+ nifIcon = 0x00000002
37
+ nifTip = 0x00000004
38
+
39
+ tpmRightButton = 0x00000002
40
+ tpmLeftAlign = 0x00000000
41
+
42
+ mfString = 0x00000000
43
+ mfSeparator = 0x00000800
44
+ mfChecked = 0x00000008
45
+ mfGrayed = 0x00000001
46
+ mfEnabled = 0x00000000
47
+ mfPopup = 0x00000010
48
+ mfByPosition = 0x00000400
49
+ )
50
+
51
+ var (
52
+ shell32 = syscall.NewLazyDLL("shell32.dll")
53
+ procShellNotifyIconW = shell32.NewProc("Shell_NotifyIconW")
54
+
55
+ procRegisterClassExW = user32win.NewProc("RegisterClassExW")
56
+ procCreateWindowExW = user32win.NewProc("CreateWindowExW")
57
+ procDefWindowProcW = user32win.NewProc("DefWindowProcW")
58
+ procCreatePopupMenu = user32win.NewProc("CreatePopupMenu")
59
+ procCreateMenu = user32win.NewProc("CreateMenu")
60
+ procAppendMenuW = user32win.NewProc("AppendMenuW")
61
+ procInsertMenuW = user32win.NewProc("InsertMenuW")
62
+ procDestroyMenu = user32win.NewProc("DestroyMenu")
63
+ procTrackPopupMenu = user32win.NewProc("TrackPopupMenu")
64
+ procGetCursorPos = user32win.NewProc("GetCursorPos")
65
+ procSetForegroundWindow = user32win.NewProc("SetForegroundWindow")
66
+ procSetMenu = user32win.NewProc("SetMenu")
67
+ procDrawMenuBar = user32win.NewProc("DrawMenuBar")
68
+ )
69
+
70
+ // ---- 结构 ----
71
+
72
+ type point struct {
73
+ X, Y int32
74
+ }
75
+
76
+ type wndClassExW struct {
77
+ CbSize uint32
78
+ Style uint32
79
+ LpfnWndProc uintptr
80
+ CbClsExtra int32
81
+ CbWndExtra int32
82
+ HInstance uintptr
83
+ HIcon uintptr
84
+ HCursor uintptr
85
+ HbrBackground uintptr
86
+ LpszMenuName *uint16
87
+ LpszClassName *uint16
88
+ HIconSm uintptr
89
+ }
90
+
91
+ // notifyIconDataW 对应 NOTIFYICONDATAW(amd64 布局,cbSize=984)。
92
+ type notifyIconDataW struct {
93
+ CbSize uint32
94
+ _ uint32 // 对齐填充
95
+ HWnd uintptr
96
+ UID uint32
97
+ UFlags uint32
98
+ UCallbackMessage uint32
99
+ HIcon uintptr
100
+ SzTip [128]uint16
101
+ DwState uint32
102
+ DwStateMask uint32
103
+ SzInfo [256]uint16
104
+ UVersion uint32
105
+ SzInfoTitle [64]uint16
106
+ DwInfoFlags uint32
107
+ GuidItem [16]byte
108
+ HBalloonIcon uintptr
109
+ }
110
+
111
+ // menuItem 描述一个托盘/菜单栏条目(JSON 透传)。
112
+ type menuItem struct {
113
+ ID string `json:"id"`
114
+ Label string `json:"label"`
115
+ Type string `json:"type"` // "item"(默认)/ "separator" / "submenu"
116
+ Enabled *bool `json:"enabled"` // nil=默认启用
117
+ Checked *bool `json:"checked"`
118
+ Submenu []menuItem `json:"submenu"`
119
+ }
120
+
121
+ // trayState 保存当前托盘实例(单窗口框架内为单例)。
122
+ type trayState struct {
123
+ mu sync.Mutex
124
+ hwnd uintptr // 隐藏消息窗口
125
+ menu uintptr // 右键 popup 菜单
126
+ menuBar uintptr // 原生菜单栏 HMENU
127
+ items map[uint32]string // menu id -> 前端 id
128
+ nextID uint32
129
+ emit func(event string, data interface{})
130
+ icon uintptr // 当前托盘图标 HICON
131
+ }
132
+
133
+ var (
134
+ trayProcMu sync.Mutex
135
+ trayStateInst *trayState
136
+ trayWndProcCB uintptr
137
+ )
138
+
139
+ // trayProcCallback 是隐藏窗口的 WndProc(Go 回调,经 syscall.NewCallback 注册)。
140
+ // 由 RegisterClassEx 注册到系统,消息泵在 UI 线程派发。
141
+ func trayProcCallback(hwnd uintptr, msg uint32, wParam, lParam uintptr) uintptr {
142
+ ts := trayStateInst
143
+ if ts == nil {
144
+ r, _, _ := procDefWindowProcW.Call(hwnd, uintptr(msg), wParam, lParam)
145
+ return r
146
+ }
147
+ switch msg {
148
+ case uint32(wmApp) + 1: // uCallbackMessage
149
+ switch lParam & 0xFFFF {
150
+ case wmLButtonUp:
151
+ ts.emit("tray:click", map[string]interface{}{"button": "left"})
152
+ case wmLButtonDbl:
153
+ ts.emit("tray:double-click", map[string]interface{}{"button": "left"})
154
+ case wmRButtonUp:
155
+ trayShowMenu(ts)
156
+ }
157
+ case uint32(wmCommand):
158
+ id := uint32(wParam & 0xFFFF)
159
+ ts.mu.Lock()
160
+ label, ok := ts.items[id]
161
+ ts.mu.Unlock()
162
+ if ok {
163
+ ts.emit("tray:menu", map[string]interface{}{"id": label})
164
+ }
165
+ }
166
+ r, _, _ := procDefWindowProcW.Call(hwnd, uintptr(msg), wParam, lParam)
167
+ return r
168
+ }
169
+
170
+ // ensureTrayWindow 创建隐藏消息窗口(仅一次)。
171
+ func ensureTrayWindow(a *App) (*trayState, error) {
172
+ trayProcMu.Lock()
173
+ defer trayProcMu.Unlock()
174
+ if trayStateInst != nil {
175
+ return trayStateInst, nil
176
+ }
177
+ className, _ := syscall.UTF16PtrFromString("FreedomTrayWnd")
178
+ if trayWndProcCB == 0 {
179
+ trayWndProcCB = syscall.NewCallback(trayProcCallback)
180
+ }
181
+ hInst := uintptr(0)
182
+ if m, _, _ := procGetModuleHandle.Call(0); m != 0 {
183
+ hInst = m
184
+ }
185
+ wc := wndClassExW{
186
+ CbSize: uint32(unsafe.Sizeof(wndClassExW{})),
187
+ LpfnWndProc: trayWndProcCB,
188
+ HInstance: hInst,
189
+ LpszClassName: className,
190
+ }
191
+ if r, _, _ := procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc))); r == 0 {
192
+ // 可能已注册,继续尝试。
193
+ }
194
+ ts := &trayState{
195
+ items: map[uint32]string{},
196
+ nextID: 1,
197
+ emit: a.Emit,
198
+ }
199
+ hwnd, _, _ := procCreateWindowExW.Call(
200
+ 0,
201
+ uintptr(unsafe.Pointer(className)),
202
+ uintptr(unsafe.Pointer(className)),
203
+ 0,
204
+ 0, 0, 0, 0,
205
+ 0, 0, hInst, 0,
206
+ )
207
+ if hwnd == 0 {
208
+ return nil, fmt.Errorf("tray: 创建隐藏窗口失败")
209
+ }
210
+ ts.hwnd = hwnd
211
+ trayStateInst = ts
212
+ return ts, nil
213
+ }
214
+
215
+ // traySetIcon 创建或更新托盘图标。
216
+ func traySetIcon(ts *trayState, iconData []byte, tooltip string) error {
217
+ ts.mu.Lock()
218
+ defer ts.mu.Unlock()
219
+ // 释放旧图标。
220
+ if ts.icon != 0 {
221
+ procDestroyIcon.Call(ts.icon)
222
+ ts.icon = 0
223
+ }
224
+ nid := &notifyIconDataW{
225
+ CbSize: uint32(unsafe.Sizeof(notifyIconDataW{})),
226
+ HWnd: ts.hwnd,
227
+ UID: 1,
228
+ UFlags: nifMessage | nifIcon | nifTip,
229
+ UCallbackMessage: wmApp + 1,
230
+ }
231
+ if len(iconData) > 0 {
232
+ ic := hiconFromPNG(iconData)
233
+ if ic == 0 {
234
+ return fmt.Errorf("tray: 无法从图标数据创建 HICON")
235
+ }
236
+ nid.HIcon = ic
237
+ ts.icon = ic
238
+ } else {
239
+ // 无图标数据:使用 exe 内嵌图标。
240
+ cx, _, _ := procGetSystemMetrics.Call(smCXSmall)
241
+ cy, _, _ := procGetSystemMetrics.Call(smCYSmall)
242
+ if ic := loadExeIcon(cx, cy); ic != 0 {
243
+ nid.HIcon = ic
244
+ ts.icon = ic
245
+ }
246
+ }
247
+ if tooltip != "" {
248
+ tp, _ := syscall.UTF16FromString(tooltip)
249
+ copy(nid.SzTip[:], tp[:min(len(tp), 127)])
250
+ }
251
+ // 首次 NIM_ADD,之后 NIM_MODIFY。
252
+ if ts.hwnd == 0 || !trayIconAdded {
253
+ trayIconAdded = true
254
+ r, _, _ := procShellNotifyIconW.Call(nimAdd, uintptr(unsafe.Pointer(nid)))
255
+ if r == 0 {
256
+ return fmt.Errorf("tray: Shell_NotifyIcon(NIM_ADD) 失败")
257
+ }
258
+ } else {
259
+ r, _, _ := procShellNotifyIconW.Call(nimModify, uintptr(unsafe.Pointer(nid)))
260
+ if r == 0 {
261
+ return fmt.Errorf("tray: Shell_NotifyIcon(NIM_MODIFY) 失败")
262
+ }
263
+ }
264
+ return nil
265
+ }
266
+
267
+ var trayIconAdded bool
268
+
269
+ // trayShowMenu 在鼠标位置弹出右键菜单。
270
+ func trayShowMenu(ts *trayState) {
271
+ if ts == nil || ts.menu == 0 {
272
+ return
273
+ }
274
+ var pt point
275
+ procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
276
+ procSetForegroundWindow.Call(ts.hwnd)
277
+ procTrackPopupMenu.Call(
278
+ ts.menu, tpmRightButton|tpmLeftAlign,
279
+ uintptr(pt.X), uintptr(pt.Y), 0, ts.hwnd, 0,
280
+ )
281
+ procPostMessage.Call(ts.hwnd, wmNull, 0, 0)
282
+ }
283
+
284
+ // buildMenu 递归构建 HMENU(用于托盘右键与菜单栏子菜单)。
285
+ // 返回菜单句柄与 id 映射(追加到 ts.items)。
286
+ func buildMenu(ts *trayState, items []menuItem) uintptr {
287
+ hMenu, _, _ := procCreatePopupMenu.Call()
288
+ if hMenu == 0 {
289
+ return 0
290
+ }
291
+ appendMenuItems(ts, hMenu, items, false)
292
+ return hMenu
293
+ }
294
+
295
+ // appendMenuItems 把条目追加到指定菜单。
296
+ func appendMenuItems(ts *trayState, hMenu uintptr, items []menuItem, isMenuBar bool) {
297
+ for i, it := range items {
298
+ if it.Type == "separator" {
299
+ procAppendMenuW.Call(hMenu, mfSeparator, 0, 0)
300
+ continue
301
+ }
302
+ if it.Type == "submenu" || (isMenuBar && len(it.Submenu) > 0) {
303
+ sub := buildMenu(ts, it.Submenu)
304
+ if sub == 0 {
305
+ continue
306
+ }
307
+ label, _ := syscall.UTF16PtrFromString(it.Label)
308
+ procAppendMenuW.Call(hMenu, mfPopup|mfEnabled, sub, uintptr(unsafe.Pointer(label)))
309
+ continue
310
+ }
311
+ var flags uintptr = mfString | mfEnabled
312
+ if it.Checked != nil && *it.Checked {
313
+ flags |= mfChecked
314
+ }
315
+ if it.Enabled != nil && !*it.Enabled {
316
+ flags |= mfGrayed
317
+ }
318
+ id := ts.nextID
319
+ ts.nextID++
320
+ ts.mu.Lock()
321
+ ts.items[id] = it.ID
322
+ ts.mu.Unlock()
323
+ label, _ := syscall.UTF16PtrFromString(it.Label)
324
+ if isMenuBar && i == 0 {
325
+ // 菜单栏首个条目用 MF_BYPOSITION 占位(顶层菜单由 SetMenu 接管)。
326
+ }
327
+ procAppendMenuW.Call(hMenu, flags, uintptr(id), uintptr(unsafe.Pointer(label)))
328
+ }
329
+ }
330
+
331
+ // traySetMenu 设置托盘右键菜单(整体替换)。
332
+ func traySetMenu(ts *trayState, items []menuItem) {
333
+ ts.mu.Lock()
334
+ old := ts.menu
335
+ ts.menu = 0
336
+ ts.mu.Unlock()
337
+ if old != 0 {
338
+ procDestroyMenu.Call(old)
339
+ }
340
+ if len(items) == 0 {
341
+ return
342
+ }
343
+ h := buildMenu(ts, items)
344
+ ts.mu.Lock()
345
+ ts.menu = h
346
+ ts.mu.Unlock()
347
+ }
348
+
349
+ // menuBarSet 设置主窗口原生菜单栏(整体替换)。
350
+ func menuBarSet(hwnd uintptr, ts *trayState, items []menuItem) {
351
+ ts.mu.Lock()
352
+ old := ts.menuBar
353
+ ts.menuBar = 0
354
+ ts.mu.Unlock()
355
+ if old != 0 {
356
+ procDestroyMenu.Call(old)
357
+ }
358
+ if len(items) == 0 {
359
+ procSetMenu.Call(hwnd, 0)
360
+ procDrawMenuBar.Call(hwnd)
361
+ return
362
+ }
363
+ hBar, _, _ := procCreateMenu.Call()
364
+ if hBar == 0 {
365
+ return
366
+ }
367
+ for _, top := range items {
368
+ label, _ := syscall.UTF16PtrFromString(top.Label)
369
+ if top.Type == "separator" {
370
+ continue
371
+ }
372
+ if len(top.Submenu) > 0 {
373
+ sub := buildMenu(ts, top.Submenu)
374
+ procAppendMenuW.Call(hBar, mfPopup|mfEnabled, sub, uintptr(unsafe.Pointer(label)))
375
+ } else {
376
+ // 顶层无子菜单:视为可点击菜单项。
377
+ var flags uintptr = mfString | mfEnabled
378
+ if top.Checked != nil && *top.Checked {
379
+ flags |= mfChecked
380
+ }
381
+ id := ts.nextID
382
+ ts.nextID++
383
+ ts.mu.Lock()
384
+ ts.items[id] = top.ID
385
+ ts.mu.Unlock()
386
+ procAppendMenuW.Call(hBar, flags, uintptr(id), uintptr(unsafe.Pointer(label)))
387
+ }
388
+ }
389
+ ts.mu.Lock()
390
+ ts.menuBar = hBar
391
+ ts.mu.Unlock()
392
+ procSetMenu.Call(hwnd, hBar)
393
+ procDrawMenuBar.Call(hwnd)
394
+ }
395
+
396
+ // trayDestroy 移除托盘图标并清理隐藏窗口。
397
+ func trayDestroy(ts *trayState) {
398
+ if ts == nil {
399
+ return
400
+ }
401
+ ts.mu.Lock()
402
+ defer ts.mu.Unlock()
403
+ if trayIconAdded {
404
+ nid := &notifyIconDataW{CbSize: uint32(unsafe.Sizeof(notifyIconDataW{})), HWnd: ts.hwnd, UID: 1}
405
+ procShellNotifyIconW.Call(nimDelete, uintptr(unsafe.Pointer(nid)))
406
+ trayIconAdded = false
407
+ }
408
+ if ts.menu != 0 {
409
+ procDestroyMenu.Call(ts.menu)
410
+ ts.menu = 0
411
+ }
412
+ if ts.menuBar != 0 {
413
+ procDestroyMenu.Call(ts.menuBar)
414
+ ts.menuBar = 0
415
+ }
416
+ if ts.icon != 0 {
417
+ procDestroyIcon.Call(ts.icon)
418
+ ts.icon = 0
419
+ }
420
+ if ts.hwnd != 0 {
421
+ procDestroyWindow.Call(ts.hwnd)
422
+ ts.hwnd = 0
423
+ }
424
+ trayStateInst = nil
425
+ }
426
+
427
+ var procDestroyWindow = user32win.NewProc("DestroyWindow")
428
+
429
+ // trayCall 处理前端 __freedom_tray 请求。
430
+ func (a *App) trayCall(method string, paramsJSON string) (result interface{}, err error) {
431
+ defer func() {
432
+ if r := recover(); r != nil {
433
+ result = nil
434
+ err = fmt.Errorf("freedom: tray method %q panicked: %v", method, r)
435
+ }
436
+ }()
437
+
438
+ var args map[string]json.RawMessage
439
+ if len(paramsJSON) > 0 && paramsJSON != "null" {
440
+ if err := json.Unmarshal([]byte(paramsJSON), &args); err != nil {
441
+ return nil, fmt.Errorf("freedom: tray method %q: invalid args: %w", method, err)
442
+ }
443
+ }
444
+ argStr := func(k string) string {
445
+ if v, ok := args[k]; ok {
446
+ var s string
447
+ if json.Unmarshal(v, &s) == nil {
448
+ return s
449
+ }
450
+ }
451
+ return ""
452
+ }
453
+
454
+ switch method {
455
+ case "tray.create":
456
+ ts, err := ensureTrayWindow(a)
457
+ if err != nil {
458
+ return nil, err
459
+ }
460
+ return nil, traySetIcon(ts, dataURLToBytes(argStr("icon")), argStr("tooltip"))
461
+ case "tray.destroy":
462
+ trayDestroy(trayStateInst)
463
+ return nil, nil
464
+ case "tray.tooltip":
465
+ ts := trayStateInst
466
+ if ts == nil {
467
+ return nil, fmt.Errorf("tray: 托盘未创建")
468
+ }
469
+ return nil, traySetIcon(ts, nil, argStr("tooltip"))
470
+ case "tray.menu":
471
+ ts := trayStateInst
472
+ if ts == nil {
473
+ return nil, fmt.Errorf("tray: 托盘未创建")
474
+ }
475
+ var items []menuItem
476
+ if v, ok := args["items"]; ok {
477
+ if err := json.Unmarshal(v, &items); err != nil {
478
+ return nil, fmt.Errorf("tray: 菜单格式错误: %w", err)
479
+ }
480
+ }
481
+ traySetMenu(ts, items)
482
+ return nil, nil
483
+ case "menu.set":
484
+ ts := trayStateInst
485
+ if ts == nil {
486
+ ts, err = ensureTrayWindow(a)
487
+ if err != nil {
488
+ return nil, err
489
+ }
490
+ }
491
+ var items []menuItem
492
+ if v, ok := args["items"]; ok {
493
+ if err := json.Unmarshal(v, &items); err != nil {
494
+ return nil, fmt.Errorf("menu: 菜单格式错误: %w", err)
495
+ }
496
+ }
497
+ hwnd := a.WindowHandle()
498
+ if hwnd == 0 {
499
+ return nil, fmt.Errorf("menu: 窗口未就绪")
500
+ }
501
+ menuBarSet(hwnd, ts, items)
502
+ return nil, nil
503
+ default:
504
+ return nil, fmt.Errorf("freedom: unknown tray method %q", method)
505
+ }
506
+ }
@@ -183,16 +183,20 @@ func exeIconData() []byte {
183
183
  if err := windows.GetModuleHandleEx(0, nil, &hInst); err != nil {
184
184
  return nil
185
185
  }
186
- // FindResourceW(hModule, MAKEINTRESOURCE(1), MAKEINTRESOURCE(RT_ICON))
187
- resInfo, err := windows.FindResource(hInst, windows.ResourceID(1), windows.ResourceID(rtIcon))
188
- if err != nil {
189
- return nil
190
- }
191
- data, err := windows.LoadResourceData(hInst, resInfo)
192
- if err != nil {
193
- return nil
186
+ // 先按 rcedit 注入约定 RT_ICON ID=1 读取;失败回退 ID=0(M3)。
187
+ // 不同注入工具写入的资源 ID 可能不同,硬编码单一 ID 会导致"标题栏图标正常
188
+ // 但 appIcon 空白"的中间态,此处与 loadExeIcon 的 ID 假设解耦。
189
+ for _, id := range []uintptr{1, 0} {
190
+ // FindResourceW(hModule, MAKEINTRESOURCE(id), MAKEINTRESOURCE(RT_ICON))
191
+ resInfo, err := windows.FindResource(hInst, windows.ResourceID(id), windows.ResourceID(rtIcon))
192
+ if err != nil {
193
+ continue
194
+ }
195
+ if data, err := windows.LoadResourceData(hInst, resInfo); err == nil {
196
+ return data
197
+ }
194
198
  }
195
- return data
199
+ return nil
196
200
  }
197
201
 
198
202
  // isPNG 判断字节流是否为 PNG 编码(rcedit 注入的 .ico 内嵌 PNG 时 RT_ICON 数据即 PNG)。
@@ -209,9 +213,15 @@ func loadExeIcon(cx, cy uintptr) uintptr {
209
213
  if hInst == 0 {
210
214
  return 0
211
215
  }
212
- // LoadImageW(hInst, MAKEINTRESOURCE(0), IMAGE_ICON, cx, cy, LR_DEFAULTCOLOR)
213
- r, _, _ := procLoadImage.Call(hInst, 0, 1 /*IMAGE_ICON*/, cx, cy, 0 /*LR_DEFAULTCOLOR*/)
214
- return r
216
+ // 先按 rcedit 注入约定 RT_GROUP_ICON ID=0 加载;失败回退 ID=1(M3:
217
+ // 与 appIcon 的 RT_ICON ID 假设解耦,兼容不同注入工具的资源 ID)。
218
+ for _, id := range []uintptr{0, 1} {
219
+ // LoadImageW(hInst, MAKEINTRESOURCE(id), IMAGE_ICON, cx, cy, LR_DEFAULTCOLOR)
220
+ if r, _, _ := procLoadImage.Call(hInst, id, 1 /*IMAGE_ICON*/, cx, cy, 0 /*LR_DEFAULTCOLOR*/); r != 0 {
221
+ return r
222
+ }
223
+ }
224
+ return 0
215
225
  }
216
226
 
217
227
  // exeAppIconDataURL 从当前 exe 的 PE 资源图标(rcedit 注入的 RT_ICON)提取为 PNG data URL。
@@ -353,10 +363,12 @@ func dibToPNG(data []byte) ([]byte, error) {
353
363
  }
354
364
  pxStart := int(biSize)
355
365
  andStart := pxStart + pxLen
356
- hasAndMask := heightRaw > 0 // 正高度表示 DIB 末尾带 AND mask
357
- if !hasAndMask {
358
- hasAndMask = andStart+andRowBytes*height <= len(data)
359
- }
366
+ // AND mask 是否存在以实际数据长度为准(H1):ICO 的 DIB 若数据不足
367
+ // (无 AND mask 段),andStart+andRowBytes*height 会超出 len(data),
368
+ // 此时下方读取 data[andRow+x/8] 将越界 panic。
369
+ // 此前以 heightRaw>0 判定恒为真(上方已排除 heightRaw==0),
370
+ // 无 mask 的 ICO 会命中越界分支导致窗口控制崩壳。
371
+ hasAndMask := andStart+andRowBytes*height <= len(data)
360
372
  img := image.NewRGBA(image.Rect(0, 0, width, height))
361
373
  for y := 0; y < height; y++ {
362
374
  srcY := height - 1 - y // DIB 自底向上:数据首行是图像末行
@@ -0,0 +1,40 @@
1
+ package main
2
+
3
+ import "unsafe"
4
+
5
+ // 写法B:widl 风格链式解引用
6
+ func comVtableB(ppv uintptr, index uint32) uintptr {
7
+ vt := *(*uintptr)(*(*unsafe.Pointer)(unsafe.Pointer(ppv)))
8
+ return *(*uintptr)(unsafe.Pointer(vt + uintptr(index)*8))
9
+ }
10
+
11
+ // 写法D:先转 unsafe.Pointer 立即传给子函数
12
+ func comVtableD(ppv uintptr, index uint32) uintptr {
13
+ return comVtableDPtr(unsafe.Pointer(ppv), index)
14
+ }
15
+
16
+ func comVtableDPtr(pp unsafe.Pointer, index uint32) uintptr {
17
+ vt := *(*uintptr)(pp)
18
+ return *(*uintptr)(unsafe.Add(unsafe.Pointer(vt), uintptr(index)*8))
19
+ }
20
+
21
+ // 写法E:uintptr 参数直接转结构体指针再读字段
22
+ type unkBox struct{ v unsafe.Pointer }
23
+
24
+ func comVtableE(ppv uintptr, index uint32) uintptr {
25
+ b := (*unkBox)(unsafe.Pointer(ppv))
26
+ slots := unsafe.Slice((*uintptr)(b.v), int(index)+1)
27
+ return slots[index]
28
+ }
29
+
30
+ // 写法F:类型化数组头 + Slice
31
+ func comVtableF(ppv uintptr, index uint32) uintptr {
32
+ obj := (*[1]unsafe.Pointer)(unsafe.Pointer(ppv))
33
+ vt := *obj
34
+ slots := unsafe.Slice((*uintptr)(vt[0]), int(index)+1)
35
+ return slots[index]
36
+ }
37
+
38
+ func main() {
39
+ _, _, _, _ = comVtableB(0, 0), comVtableD(0, 0), comVtableE(0, 0), comVtableF(0, 0)
40
+ }
File without changes
@@ -0,0 +1 @@
1
+ pkg\freedom\syscap_windows.go:56:30: possible misuse of unsafe.Pointer
@@ -0,0 +1,2 @@
1
+ pkg\freedom\syscap_windows.go:56:22: possible misuse of unsafe.Pointer
2
+ pkg\freedom\syscap_windows.go:61:32: possible misuse of unsafe.Pointer
@@ -18,6 +18,14 @@ export default {
18
18
  // 是否开启 WebView 开发者工具。
19
19
  debug: false,
20
20
 
21
+ // 安全模式,在终端随时可改,三选一:
22
+ // 'none' - 明文资源(默认,兼容历史产物)
23
+ // 'basic' - 明文资源 + 构建期加固提示
24
+ // 'high' - resources 整体加密为 app.bin(磁盘无明文 HTML/配置),
25
+ // 壳内存解密 + 完整性校验,另含 anti-debug / 进程隐藏
26
+ // 终端命令:freedom security <none|basic|high>,或在 freedom build 时传 --security <mode>
27
+ security: 'none',
28
+
21
29
  // 标题栏策略,在终端随时可改,二选一:
22
30
  // 'native' - 保留系统原生标题栏,标题栏图标与 exe 图标一致
23
31
  // 'frameless' - 完全无边框,标题栏与 Windows 原生最小化 / 最大化 / 关闭按钮均不存在,
@@ -0,0 +1,58 @@
1
+ // Freedom 应用配置(极简模板)。
2
+ // 构建时由 freedom CLI 读取,生成对应的壳层配置。
3
+ export default {
4
+ // 应用名(窗口标题 / 可执行文件名)。
5
+ name: 'freedom-app',
6
+
7
+ // 窗口初始尺寸(像素)。
8
+ width: 1024,
9
+ height: 720,
10
+
11
+ // 窗口最小尺寸(设为 0 表示不限制)。
12
+ minWidth: 400,
13
+ minHeight: 300,
14
+
15
+ // 启动时是否在屏幕居中。
16
+ center: true,
17
+
18
+ // 是否开启 WebView 开发者工具。
19
+ debug: false,
20
+
21
+ // 安全模式,在终端随时可改,三选一:
22
+ // 'none' - 明文资源(默认,兼容历史产物)
23
+ // 'basic' - 明文资源 + 构建期加固提示
24
+ // 'high' - resources 整体加密为 app.bin(磁盘无明文 HTML/配置),
25
+ // 壳内存解密 + 完整性校验,另含 anti-debug / 进程隐藏
26
+ // 终端命令:freedom security <none|basic|high>,或在 freedom build 时传 --security <mode>
27
+ security: 'none',
28
+
29
+ // 标题栏策略,在终端随时可改,二选一:
30
+ // 'native' - 保留系统原生标题栏(极简模板默认:不引入自绘标题栏,开箱即用)
31
+ // 'frameless' - 完全无边框,关闭 / 最大化 / 最小化按钮由前端自绘。
32
+ // 切换 frameless 后需在前端自行实现窗口控制按钮(完整示例见完整模板 full:
33
+ // freedom init --template full 生成的 index.html / src/main.js)。
34
+ // 终端命令:freedom titlebar <native|frameless>
35
+ titlebar: 'native',
36
+
37
+ // 应用图标(可执行文件 / .app 的图标):
38
+ // Windows 需 .ico(推荐含 16/32/48/256 多尺寸),构建时注入 exe 资源;
39
+ // macOS 需 .icns,构建时放入 .app/Contents/Resources 并写入 Info.plist。
40
+ // 值为相对项目根目录的路径或绝对路径,未配置则使用壳默认图标。
41
+ // 终端命令:freedom icon <path>(Windows 主用)
42
+ icon: undefined,
43
+
44
+ // 产物输出目录(相对项目根目录或绝对路径):
45
+ // 'dist' - 输出到 dist/ 子目录(默认)
46
+ // '.' - 直接输出到项目根目录(dist 的上级),exe 与单文件 index.html 就地生成
47
+ // 'build/app' 等任意路径亦可
48
+ // 注意:输出到项目根目录时不会覆盖项目源文件(index.html 仅保留 src 源文件)。
49
+ outDir: 'dist',
50
+
51
+ // 后端策略:
52
+ // undefined - 仅使用框架内置能力(ping / 窗口控制),不启动独立后端进程
53
+ // { command, args } - 启动任意语言后端进程(Go/Node/Python/Rust 均可),
54
+ // 经 stdin/stdout NDJSON 与壳通信,方法直接暴露给前端
55
+ // 示例(Node 后端):
56
+ // backend: { command: 'node', args: ['backend/main.mjs'] }
57
+ // backend: undefined,
58
+ };