cj-tauri 2.0.0 → 2.1.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.
Files changed (158) hide show
  1. package/README.md +113 -99
  2. package/bin/cj-tauri.js +287 -287
  3. package/cjpm.toml +63 -52
  4. package/cli/cjpm.toml +22 -22
  5. package/cli/src/doctor.cj +308 -306
  6. package/cli/src/main.cj +129 -129
  7. package/cli/src/manifest.cj +632 -558
  8. package/cli/src/manifest_test.cj +87 -0
  9. package/cli/src/project.cj +770 -770
  10. package/cli/src/resolve.cj +348 -348
  11. package/cli/src/scaffold.cj +369 -369
  12. package/cli/src/sync.cj +526 -526
  13. package/cli/src/util.cj +168 -168
  14. package/cli/templates/app/README.md +51 -48
  15. package/cli/templates/app/assets/capabilities/default.json +7 -7
  16. package/cli/templates/app/assets/ui/index.html +120 -118
  17. package/cli/templates/app/cj-tauri.toml +32 -22
  18. package/cli/templates/app/cjpm.toml +34 -34
  19. package/cli/templates/app/src/main.cj +101 -90
  20. package/cli/templates/app-react/README.md +56 -56
  21. package/cli/templates/app-react/assets/capabilities/default.json +7 -7
  22. package/cli/templates/app-react/assets/ui/index.html +25 -25
  23. package/cli/templates/app-react/assets/ui/package.json +19 -19
  24. package/cli/templates/app-react/assets/ui/src/App.css +67 -67
  25. package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
  26. package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
  27. package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
  28. package/cli/templates/app-react/cj-tauri.toml +33 -23
  29. package/cli/templates/app-react/cjpm.toml +34 -34
  30. package/cli/templates/app-react/src/main.cj +91 -91
  31. package/cli/templates/app-vue/README.md +56 -56
  32. package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
  33. package/cli/templates/app-vue/assets/ui/index.html +25 -25
  34. package/cli/templates/app-vue/assets/ui/package.json +18 -18
  35. package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
  36. package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
  37. package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
  38. package/cli/templates/app-vue/cj-tauri.toml +33 -23
  39. package/cli/templates/app-vue/cjpm.toml +34 -34
  40. package/cli/templates/app-vue/src/main.cj +91 -91
  41. package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
  42. package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
  43. package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
  44. package/cli/templates/ohos-skeleton/README.md +84 -79
  45. package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
  46. package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
  47. package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
  48. package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
  49. package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
  50. package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
  51. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
  52. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
  53. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
  54. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
  55. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
  56. package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
  57. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
  58. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
  59. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
  60. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
  61. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
  62. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
  63. package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
  64. package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
  65. package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
  66. package/native/build_win.bat +62 -62
  67. package/native/core/bridge_core.c +1681 -1626
  68. package/native/core/bridge_core.h +675 -618
  69. package/native/core/bridge_js.h +108 -108
  70. package/native/platforms/harmonyos/README.md +115 -6
  71. package/native/platforms/linux/bridge_linux.c +2787 -2485
  72. package/native/platforms/windows/bridge_win.c +3740 -3248
  73. package/native/tests/gtk_threading_probe.c +143 -143
  74. package/native/tests/test_bridge_core.c +1339 -1228
  75. package/package.json +53 -53
  76. package/prebuilt/linux-x64/cj-tauri +0 -0
  77. package/src/api_system.cj +67 -67
  78. package/src/app.cj +325 -969
  79. package/src/app_events.cj +166 -0
  80. package/src/app_host_ops.cj +287 -0
  81. package/src/app_run.cj +283 -0
  82. package/src/app_spec.cj +76 -76
  83. package/src/arkweb_menu.cj +216 -0
  84. package/src/arkweb_script.cj +142 -142
  85. package/src/capability.cj +409 -409
  86. package/src/clipboard_bridge.cj +178 -0
  87. package/src/dialog_spec.cj +79 -0
  88. package/src/dragdrop.cj +26 -26
  89. package/src/embedded_assets.cj +98 -98
  90. package/src/host.cj +752 -607
  91. package/src/host_arkweb.cj +924 -778
  92. package/src/host_webkit.cj +663 -570
  93. package/src/host_webview2.cj +663 -570
  94. package/src/hotkey.cj +66 -66
  95. package/src/ipc_hub.cj +337 -337
  96. package/src/ipc_message.cj +286 -286
  97. package/src/launch.cj +62 -62
  98. package/src/logging.cj +127 -127
  99. package/src/menu.cj +197 -197
  100. package/src/platform/platform_app_context.cj +77 -77
  101. package/src/platform/platform_assets.cj +33 -33
  102. package/src/platform/platform_discover.cj +176 -176
  103. package/src/platform/platform_env.cj +29 -29
  104. package/src/platform/platform_feature.cj +31 -31
  105. package/src/platform/platform_page_source.cj +87 -87
  106. package/src/platform/platform_target_info.cj +32 -32
  107. package/src/platform_host_provider.cj +87 -87
  108. package/src/plugin.cj +333 -333
  109. package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
  110. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  111. package/src/plugins/fs/plugin_fs.cj +105 -1
  112. package/src/plugins/http/http_backend.cj +131 -0
  113. package/src/plugins/http/http_desktop.cj +195 -0
  114. package/src/plugins/http/http_ohos.cj +204 -0
  115. package/src/plugins/http/plugin_http.cj +310 -459
  116. package/src/plugins/log/plugin_log.cj +105 -105
  117. package/src/plugins/menu/plugin_menu.cj +158 -158
  118. package/src/plugins/notification/plugin_notification.cj +154 -154
  119. package/src/plugins/serial/plugin_serial.cj +458 -454
  120. package/src/plugins/serial/serial_bridge.cj +194 -194
  121. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  122. package/src/plugins/serial/serial_ohos.cj +529 -0
  123. package/src/plugins/shell/plugin_shell.cj +258 -258
  124. package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
  125. package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
  126. package/src/plugins/ws/plugin_ws.cj +488 -470
  127. package/src/process_hidden.cj +174 -174
  128. package/src/scope.cj +391 -391
  129. package/src/shell_event.cj +69 -69
  130. package/src/tests/app_spec_test.cj +126 -127
  131. package/src/tests/capability_test.cj +243 -243
  132. package/src/tests/dialog_spec_test.cj +73 -0
  133. package/src/tests/fake_host.cj +201 -201
  134. package/src/tests/ipc_hub_test.cj +487 -487
  135. package/src/tests/logging_test.cj +62 -62
  136. package/src/tests/platform_contract_test.cj +251 -251
  137. package/src/tests/plugin_clipboard_test.cj +222 -222
  138. package/src/tests/plugin_dialog_test.cj +237 -237
  139. package/src/tests/plugin_fs_test.cj +207 -0
  140. package/src/tests/plugin_http_test.cj +124 -124
  141. package/src/tests/plugin_notification_test.cj +205 -205
  142. package/src/tests/plugin_serial_test.cj +48 -0
  143. package/src/tests/plugin_test.cj +605 -585
  144. package/src/tests/plugin_ws_test.cj +40 -40
  145. package/src/tests/process_hidden_test.cj +129 -129
  146. package/src/tests/scope_test.cj +350 -350
  147. package/src/tests/system_host_test.cj +99 -81
  148. package/src/tests/urlutil_test.cj +53 -53
  149. package/src/tests/version_test.cj +75 -75
  150. package/src/tests/window_registry_test.cj +123 -123
  151. package/src/tray.cj +90 -90
  152. package/src/urlutil.cj +159 -159
  153. package/src/version.cj +44 -44
  154. package/src/window.cj +51 -51
  155. package/src/window_registry.cj +116 -116
  156. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  157. package/src/plugins/serial/serial_ohos_stub.cj +0 -61
  158. package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
package/src/capability.cj CHANGED
@@ -1,409 +1,409 @@
1
- package cjTauri
2
-
3
- import std.collection.*
4
- import std.sync.*
5
- import stdx.encoding.json.*
6
-
7
- /**
8
- * 能力安全模型(对标 Tauri capability)
9
- *
10
- * capabilities/default.json 示例:
11
- * {
12
- * "identifier": "default",
13
- * "windows": ["main"],
14
- * "commands": ["greet", "system:version", "system:ping"],
15
- * "events": ["tick"],
16
- * "permissions": ["fs:readonly"]
17
- * }
18
- *
19
- * 默认最小权限:未声明的命令一律拒绝,未声明的事件不投递。
20
- *
21
- * permissions(v1.1):命名权限集——插件声明「集名 → 成员」,应用按集名引用;
22
- * 集名形如 `<插件名>:<短集名>`(如 fs:readonly)。成员展开后与 commands/events 里的明文名字同权,
23
- * 所以旧清单(全写明文)行为完全不变。集名没被任何插件提供时只是不生效、不报错,
24
- * 启动时由 warnUnknownPermissionSets 提示(多半是拼错或忘了装配插件)。
25
- */
26
- public class Capability {
27
- public let identifier: String
28
- public let windows: ArrayList<String>
29
- public let commands: HashSet<String>
30
- public let events: HashSet<String>
31
- /** 引用到的命名权限集(原始名,如 "fs:readonly");展开规则见 CapabilityRegistry */
32
- public let permissions: HashSet<String>
33
- /**
34
- * 路径 scope(RFC-003 第 4 期):插件名 → 路径白名单规则。
35
- * 没有某插件的条目 = 该插件按旧行为全量放行(启动时有 stderr 提示);
36
- * 有条目就走「先 deny 后 allow」的二次校验,见 src/scope.cj。
37
- */
38
- public let scopes: HashMap<String, ScopeRule>
39
-
40
- public init(identifier: String) {
41
- this.identifier = identifier
42
- this.windows = ArrayList<String>()
43
- this.commands = HashSet<String>()
44
- this.events = HashSet<String>()
45
- this.permissions = HashSet<String>()
46
- this.scopes = HashMap<String, ScopeRule>()
47
- }
48
-
49
- public func allowCommand(cmd: String): Unit {
50
- this.commands.add(cmd)
51
- }
52
-
53
- /** 引用一个命名权限集(集由插件在装配时注册,见 plugin.cj 的 permissions()) */
54
- public func allowPermissionSet(name: String): Unit {
55
- this.permissions.add(name)
56
- }
57
-
58
- public func allowEvent(event: String): Unit {
59
- this.events.add(event)
60
- }
61
-
62
- public func allowWindow(win: String): Unit {
63
- this.windows.add(win)
64
- }
65
-
66
- public func canInvoke(cmd: String): Bool {
67
- return this.commands.contains(cmd)
68
- }
69
-
70
- public func canEmit(event: String): Bool {
71
- return this.events.contains(event)
72
- }
73
-
74
- /**
75
- * 从 JSON 解析 capability 清单
76
- */
77
- public static func fromJson(json: JsonValue): Option<Capability> {
78
- try {
79
- match (json.kind()) {
80
- case JsonKind.JsObject => ()
81
- case _ => return None
82
- }
83
- let obj = json.asObject()
84
- let idV = obj.get("identifier")
85
- if (idV.isNone()) {
86
- return None
87
- }
88
- match (idV.getOrThrow().kind()) {
89
- case JsonKind.JsString => ()
90
- case _ => return None
91
- }
92
- let cap = Capability(idV.getOrThrow().asString().getValue())
93
-
94
- // commands
95
- if (let Some(cmds) <- obj.get("commands")) {
96
- match (cmds.kind()) {
97
- case JsonKind.JsArray =>
98
- let arr = cmds.asArray()
99
- for (i in 0..arr.size()) {
100
- if (let Some(c) <- arr.get(i)) {
101
- match (c.kind()) {
102
- case JsonKind.JsString => cap.allowCommand(c.asString().getValue())
103
- case _ => ()
104
- }
105
- }
106
- }
107
- case _ => ()
108
- }
109
- }
110
- // events
111
- if (let Some(evts) <- obj.get("events")) {
112
- match (evts.kind()) {
113
- case JsonKind.JsArray =>
114
- let arr = evts.asArray()
115
- for (i in 0..arr.size()) {
116
- if (let Some(e) <- arr.get(i)) {
117
- match (e.kind()) {
118
- case JsonKind.JsString => cap.allowEvent(e.asString().getValue())
119
- case _ => ()
120
- }
121
- }
122
- }
123
- case _ => ()
124
- }
125
- }
126
- // permissions:命名权限集(v1.1)——集名由插件提供,成员展开后与 commands/events 同权
127
- if (let Some(perms) <- obj.get("permissions")) {
128
- match (perms.kind()) {
129
- case JsonKind.JsArray =>
130
- let arr = perms.asArray()
131
- for (i in 0..arr.size()) {
132
- if (let Some(x) <- arr.get(i)) {
133
- match (x.kind()) {
134
- case JsonKind.JsString => cap.allowPermissionSet(x.asString().getValue())
135
- case _ => ()
136
- }
137
- }
138
- }
139
- case _ => ()
140
- }
141
- }
142
- // windows
143
- if (let Some(wins) <- obj.get("windows")) {
144
- match (wins.kind()) {
145
- case JsonKind.JsArray =>
146
- let arr = wins.asArray()
147
- for (i in 0..arr.size()) {
148
- if (let Some(w) <- arr.get(i)) {
149
- match (w.kind()) {
150
- case JsonKind.JsString => cap.allowWindow(w.asString().getValue())
151
- case _ => ()
152
- }
153
- }
154
- }
155
- case _ => ()
156
- }
157
- }
158
- // scopes:插件名 → 路径白名单(RFC-003 第 4 期)。键不认识也照收——
159
- // 判定只在「插件真的来问」时发生,写错的键不会放行任何东西,只会让本该严的规则不生效
160
- if (let Some(scopes) <- obj.get("scopes")) {
161
- match (scopes.kind()) {
162
- case JsonKind.JsObject =>
163
- // JsonObject 没有 keys(),取 fields 这张表来遍历(plugin_store.cj 同样写法)
164
- for ((k, v) in scopes.asObject().getFields()) {
165
- if (let Some(rule) <- parseScopeRule(v)) {
166
- cap.scopes.add(k, rule)
167
- }
168
- }
169
- case _ => ()
170
- }
171
- }
172
- return Some(cap)
173
- } catch (e: Exception) {
174
- return None
175
- }
176
- }
177
- }
178
-
179
- /**
180
- * 能力注册中心:聚合多个 capability 清单,提供统一校验
181
- */
182
- public class CapabilityRegistry {
183
- private var caps: ArrayList<Capability> = ArrayList<Capability>()
184
- // 命名权限集:集名("fs:readonly")→ 成员全名。由插件装配时注册(plugin.cj 的 registerPluginPermissionSets)
185
- private var permissionSets: HashMap<String, HashSet<String>> = HashMap<String, HashSet<String>>()
186
- // 运行期叠加的 scope 白名单(插件名 → 模式):由应用**取得用户明确确认后**经
187
- // TauriApp.allowScopePattern 追加(如原生确认框问到「是」)。只在内存里,重启即失效——
188
- // 持久化是应用自己的事(决定权在应用:写哪、写不写)。命令分发是异步的(worker 线程读
189
- // scope),所以叠加与读取共用同一把锁。
190
- private var runtimeScopeAllow: HashMap<String, ArrayList<String>> = HashMap<String, ArrayList<String>>()
191
- private let scopeLock: Mutex = Mutex()
192
-
193
- public init() {}
194
-
195
- /**
196
- * 注册一个命名权限集(同集名可多次注册,成员取并集)
197
- */
198
- public func addPermissionSet(name: String, members: ArrayList<String>): Unit {
199
- if (!this.permissionSets.contains(name)) {
200
- this.permissionSets.add(name, HashSet<String>())
201
- }
202
- let set = this.permissionSets.get(name).getOrThrow()
203
- for (m in members) {
204
- set.add(m)
205
- }
206
- }
207
-
208
- public func hasPermissionSet(name: String): Bool {
209
- return this.permissionSets.contains(name)
210
- }
211
-
212
- /**
213
- * 清单里引用过、但可能没被任何插件提供的集名(去重)。
214
- * 供启动提示用:未知集名只是不生效,不是错误——但多半是拼错了。
215
- */
216
- public func declaredPermissionSets(): ArrayList<String> {
217
- let seen = HashSet<String>()
218
- let names = ArrayList<String>()
219
- for (c in this.caps) {
220
- for (n in c.permissions) {
221
- if (!seen.contains(n)) {
222
- seen.add(n)
223
- names.add(n)
224
- }
225
- }
226
- }
227
- return names
228
- }
229
-
230
- /**
231
- * name 是否被「某条清单引用的某个集」覆盖。
232
- * 单层展开:集成员若又写成别的集名,不会被继续展开(避免环)。
233
- */
234
- private func coveredByPermissionSet(name: String): Bool {
235
- for (c in this.caps) {
236
- for (setName in c.permissions) {
237
- match (this.permissionSets.get(setName)) {
238
- case Some(members) =>
239
- if (members.contains(name)) {
240
- return true
241
- }
242
- case None => ()
243
- }
244
- }
245
- }
246
- return false
247
- }
248
-
249
- public func add(cap: Capability): Unit {
250
- this.caps.add(cap)
251
- }
252
-
253
- public func addFromJson(json: JsonValue): Bool {
254
- if (let Some(cap) <- Capability.fromJson(json)) {
255
- this.add(cap)
256
- return true
257
- }
258
- return false
259
- }
260
-
261
- public func canInvoke(cmd: String): Bool {
262
- for (c in this.caps) {
263
- if (c.canInvoke(cmd)) {
264
- return true
265
- }
266
- }
267
- // 命名权限集算放行:集内成员与写明文同权(v1.1)
268
- return this.coveredByPermissionSet(cmd)
269
- }
270
-
271
- public func canEmit(event: String): Bool {
272
- for (c in this.caps) {
273
- if (c.canEmit(event)) {
274
- return true
275
- }
276
- }
277
- return this.coveredByPermissionSet(event)
278
- }
279
-
280
- /**
281
- * 某个插件的路径 scope(RFC-003 第 4 期):把所有清单里给它的规则**并集**起来。
282
- * 返回 None = 没有任何清单为它写 scope → 调用方按旧行为全量放行。
283
- *
284
- * 为什么是并集而不是「逐清单分别判定」:与 canInvoke / canEmit 的口径一致——
285
- * 现有能力模型就是「任一清单放行即放行」,scope 跟着同一条口径走才解释得清。
286
- * (列级 + 窗口级 scopes 是 Tauri v2 更细的模型,本轮不做,见 RFC-003 第 4 期。)
287
- */
288
- public func scopeForPlugin(plugin: String): Option<ScopeRule> {
289
- var found = false
290
- let merged = ScopeRule()
291
- for (c in this.caps) {
292
- match (c.scopes.get(plugin)) {
293
- case Some(rule) =>
294
- found = true
295
- for (a in rule.allow) {
296
- merged.allow.add(a)
297
- }
298
- for (d in rule.deny) {
299
- merged.deny.add(d)
300
- }
301
- case None => ()
302
- }
303
- }
304
- if (!found) {
305
- return None
306
- }
307
- // 运行期叠加的白名单与清单成员同权(放在这里,是为了守住「注册中心是唯一判定口径」——
308
- // 各插件的 scope 判定全都经 scopeForPlugin,不必各自再查一遍运行期表)。
309
- this.mergeRuntimeAllow(plugin, merged)
310
- return Some(merged)
311
- }
312
-
313
- /**
314
- * 运行期给某插件的 scope 追加一条 allow 模式(插件名如 "http",模式写法同 `scope.cj`)。
315
- *
316
- * 用途:应用**取得用户明确确认后**(如弹原生确认框)放行一个清单里没有的主机,让本次会话当场可用。
317
- * 判定顺序不变:**清单里的 deny 命中即拒,运行期加白掀不掉它**。
318
- *
319
- * **只收「具体值」,不收通配**:模式里出现 `*` 或 `?` 一律拒绝(返回 false + 一行 stderr)。
320
- * 加白的语义是「往已有白名单里加一条」,而模式是页面可控字符串——一个通配透传进来就等价于全放行,
321
- * 整条防线当场作废。这条不变量放在注册中心里强制,任何应用都绕不过去(D-021 的代价第 1 条)。
322
- */
323
- public func addRuntimeScopeAllow(plugin: String, pattern: String): Bool {
324
- if (!isConcreteScopeValue(pattern)) {
325
- eprintln("[cj-tauri] runtime scope allow refused (${plugin}): pattern must be a concrete value, got '${pattern}'")
326
- return false
327
- }
328
- synchronized(this.scopeLock) {
329
- if (!this.runtimeScopeAllow.contains(plugin)) {
330
- this.runtimeScopeAllow.add(plugin, ArrayList<String>())
331
- }
332
- let list = this.runtimeScopeAllow.get(plugin).getOrThrow()
333
- if (!list.contains(pattern)) {
334
- list.add(pattern)
335
- }
336
- }
337
- return true
338
- }
339
-
340
- /**
341
- * 运行期加白只收「不含通配的具体值」:`*` / `?` 会让模式退化成「全放行」,制表 / 换行 / 回车
342
- * 则几乎总是「从文件或页面读回来时带了脏字符」,两类都拒(口径见 `addRuntimeScopeAllow`)。
343
- * 注意 `String` 迭代出来的是 UTF-8 **字节**而不是码点,这里判的正是字节字面量。
344
- */
345
- private func isConcreteScopeValue(pattern: String): Bool {
346
- if (pattern.size == 0) {
347
- return false
348
- }
349
- for (b in pattern) {
350
- if (b == 42u8 || b == 63u8 || b == 9u8 || b == 10u8 || b == 13u8) {
351
- return false
352
- }
353
- }
354
- return true
355
- }
356
-
357
- /** 某插件的 scope 此刻是否放行某个值(清单 + 运行期叠加;没写 scope 的插件恒放行=旧行为) */
358
- public func scopeAllowsValue(plugin: String, value: String): Bool {
359
- match (this.scopeForPlugin(plugin)) {
360
- case Some(rule) => return scopeAllows(rule, value)
361
- case None => return true
362
- }
363
- }
364
-
365
- /**
366
- * 把运行期叠加项并进合并结果。**只有清单已为该插件写过 scope 时才并**:
367
- * 没写 scope 的插件本来是全量放行(scopeForPlugin 返 None,调用方走旧行为),
368
- * 这时把运行期条目并进去会把它意外收窄成「只放行这一条」。
369
- */
370
- private func mergeRuntimeAllow(plugin: String, merged: ScopeRule): Unit {
371
- synchronized(this.scopeLock) {
372
- match (this.runtimeScopeAllow.get(plugin)) {
373
- case Some(extra) =>
374
- for (a in extra) {
375
- merged.allow.add(a)
376
- }
377
- case None => ()
378
- }
379
- }
380
- }
381
-
382
- /** 是否有任何清单为某插件声明过 scope(启动提示与判定分支共用这一个口径) */
383
- public func hasScopeForPlugin(plugin: String): Bool {
384
- for (c in this.caps) {
385
- if (c.scopes.contains(plugin)) {
386
- return true
387
- }
388
- }
389
- return false
390
- }
391
-
392
- public func size(): Int64 {
393
- return this.caps.size
394
- }
395
- }
396
-
397
- /**
398
- * 提示清单里引用了、但没有任何插件提供的命名权限集。
399
- * 只提示不改行为:未知集名不生效(默认最小权限照旧),其它命令与事件不受影响。
400
- * 调用时机:插件装配完成之后(那时能提供的集才都注册完)。
401
- */
402
- public func warnUnknownPermissionSets(reg: CapabilityRegistry): Unit {
403
- for (name in reg.declaredPermissionSets()) {
404
- if (!reg.hasPermissionSet(name)) {
405
- eprintln("[cj-tauri] 未知的命名权限集 \"${name}\":没有插件提供它,这一条不生效")
406
- eprintln("[cj-tauri] 检查拼写;若它来自插件,确认提供它的插件已 .plugin(...) 装配")
407
- }
408
- }
409
- }
1
+ package cjTauri
2
+
3
+ import std.collection.*
4
+ import std.sync.*
5
+ import stdx.encoding.json.*
6
+
7
+ /**
8
+ * 能力安全模型(对标 Tauri capability)
9
+ *
10
+ * capabilities/default.json 示例:
11
+ * {
12
+ * "identifier": "default",
13
+ * "windows": ["main"],
14
+ * "commands": ["greet", "system:version", "system:ping"],
15
+ * "events": ["tick"],
16
+ * "permissions": ["fs:readonly"]
17
+ * }
18
+ *
19
+ * 默认最小权限:未声明的命令一律拒绝,未声明的事件不投递。
20
+ *
21
+ * permissions(v1.1):命名权限集——插件声明「集名 → 成员」,应用按集名引用;
22
+ * 集名形如 `<插件名>:<短集名>`(如 fs:readonly)。成员展开后与 commands/events 里的明文名字同权,
23
+ * 所以旧清单(全写明文)行为完全不变。集名没被任何插件提供时只是不生效、不报错,
24
+ * 启动时由 warnUnknownPermissionSets 提示(多半是拼错或忘了装配插件)。
25
+ */
26
+ public class Capability {
27
+ public let identifier: String
28
+ public let windows: ArrayList<String>
29
+ public let commands: HashSet<String>
30
+ public let events: HashSet<String>
31
+ /** 引用到的命名权限集(原始名,如 "fs:readonly");展开规则见 CapabilityRegistry */
32
+ public let permissions: HashSet<String>
33
+ /**
34
+ * 路径 scope(RFC-003 第 4 期):插件名 → 路径白名单规则。
35
+ * 没有某插件的条目 = 该插件按旧行为全量放行(启动时有 stderr 提示);
36
+ * 有条目就走「先 deny 后 allow」的二次校验,见 src/scope.cj。
37
+ */
38
+ public let scopes: HashMap<String, ScopeRule>
39
+
40
+ public init(identifier: String) {
41
+ this.identifier = identifier
42
+ this.windows = ArrayList<String>()
43
+ this.commands = HashSet<String>()
44
+ this.events = HashSet<String>()
45
+ this.permissions = HashSet<String>()
46
+ this.scopes = HashMap<String, ScopeRule>()
47
+ }
48
+
49
+ public func allowCommand(cmd: String): Unit {
50
+ this.commands.add(cmd)
51
+ }
52
+
53
+ /** 引用一个命名权限集(集由插件在装配时注册,见 plugin.cj 的 permissions()) */
54
+ public func allowPermissionSet(name: String): Unit {
55
+ this.permissions.add(name)
56
+ }
57
+
58
+ public func allowEvent(event: String): Unit {
59
+ this.events.add(event)
60
+ }
61
+
62
+ public func allowWindow(win: String): Unit {
63
+ this.windows.add(win)
64
+ }
65
+
66
+ public func canInvoke(cmd: String): Bool {
67
+ return this.commands.contains(cmd)
68
+ }
69
+
70
+ public func canEmit(event: String): Bool {
71
+ return this.events.contains(event)
72
+ }
73
+
74
+ /**
75
+ * 从 JSON 解析 capability 清单
76
+ */
77
+ public static func fromJson(json: JsonValue): Option<Capability> {
78
+ try {
79
+ match (json.kind()) {
80
+ case JsonKind.JsObject => ()
81
+ case _ => return None
82
+ }
83
+ let obj = json.asObject()
84
+ let idV = obj.get("identifier")
85
+ if (idV.isNone()) {
86
+ return None
87
+ }
88
+ match (idV.getOrThrow().kind()) {
89
+ case JsonKind.JsString => ()
90
+ case _ => return None
91
+ }
92
+ let cap = Capability(idV.getOrThrow().asString().getValue())
93
+
94
+ // commands
95
+ if (let Some(cmds) <- obj.get("commands")) {
96
+ match (cmds.kind()) {
97
+ case JsonKind.JsArray =>
98
+ let arr = cmds.asArray()
99
+ for (i in 0..arr.size()) {
100
+ if (let Some(c) <- arr.get(i)) {
101
+ match (c.kind()) {
102
+ case JsonKind.JsString => cap.allowCommand(c.asString().getValue())
103
+ case _ => ()
104
+ }
105
+ }
106
+ }
107
+ case _ => ()
108
+ }
109
+ }
110
+ // events
111
+ if (let Some(evts) <- obj.get("events")) {
112
+ match (evts.kind()) {
113
+ case JsonKind.JsArray =>
114
+ let arr = evts.asArray()
115
+ for (i in 0..arr.size()) {
116
+ if (let Some(e) <- arr.get(i)) {
117
+ match (e.kind()) {
118
+ case JsonKind.JsString => cap.allowEvent(e.asString().getValue())
119
+ case _ => ()
120
+ }
121
+ }
122
+ }
123
+ case _ => ()
124
+ }
125
+ }
126
+ // permissions:命名权限集(v1.1)——集名由插件提供,成员展开后与 commands/events 同权
127
+ if (let Some(perms) <- obj.get("permissions")) {
128
+ match (perms.kind()) {
129
+ case JsonKind.JsArray =>
130
+ let arr = perms.asArray()
131
+ for (i in 0..arr.size()) {
132
+ if (let Some(x) <- arr.get(i)) {
133
+ match (x.kind()) {
134
+ case JsonKind.JsString => cap.allowPermissionSet(x.asString().getValue())
135
+ case _ => ()
136
+ }
137
+ }
138
+ }
139
+ case _ => ()
140
+ }
141
+ }
142
+ // windows
143
+ if (let Some(wins) <- obj.get("windows")) {
144
+ match (wins.kind()) {
145
+ case JsonKind.JsArray =>
146
+ let arr = wins.asArray()
147
+ for (i in 0..arr.size()) {
148
+ if (let Some(w) <- arr.get(i)) {
149
+ match (w.kind()) {
150
+ case JsonKind.JsString => cap.allowWindow(w.asString().getValue())
151
+ case _ => ()
152
+ }
153
+ }
154
+ }
155
+ case _ => ()
156
+ }
157
+ }
158
+ // scopes:插件名 → 路径白名单(RFC-003 第 4 期)。键不认识也照收——
159
+ // 判定只在「插件真的来问」时发生,写错的键不会放行任何东西,只会让本该严的规则不生效
160
+ if (let Some(scopes) <- obj.get("scopes")) {
161
+ match (scopes.kind()) {
162
+ case JsonKind.JsObject =>
163
+ // JsonObject 没有 keys(),取 fields 这张表来遍历(plugin_store.cj 同样写法)
164
+ for ((k, v) in scopes.asObject().getFields()) {
165
+ if (let Some(rule) <- parseScopeRule(v)) {
166
+ cap.scopes.add(k, rule)
167
+ }
168
+ }
169
+ case _ => ()
170
+ }
171
+ }
172
+ return Some(cap)
173
+ } catch (e: Exception) {
174
+ return None
175
+ }
176
+ }
177
+ }
178
+
179
+ /**
180
+ * 能力注册中心:聚合多个 capability 清单,提供统一校验
181
+ */
182
+ public class CapabilityRegistry {
183
+ private var caps: ArrayList<Capability> = ArrayList<Capability>()
184
+ // 命名权限集:集名("fs:readonly")→ 成员全名。由插件装配时注册(plugin.cj 的 registerPluginPermissionSets)
185
+ private var permissionSets: HashMap<String, HashSet<String>> = HashMap<String, HashSet<String>>()
186
+ // 运行期叠加的 scope 白名单(插件名 → 模式):由应用**取得用户明确确认后**经
187
+ // TauriApp.allowScopePattern 追加(如原生确认框问到「是」)。只在内存里,重启即失效——
188
+ // 持久化是应用自己的事(决定权在应用:写哪、写不写)。命令分发是异步的(worker 线程读
189
+ // scope),所以叠加与读取共用同一把锁。
190
+ private var runtimeScopeAllow: HashMap<String, ArrayList<String>> = HashMap<String, ArrayList<String>>()
191
+ private let scopeLock: Mutex = Mutex()
192
+
193
+ public init() {}
194
+
195
+ /**
196
+ * 注册一个命名权限集(同集名可多次注册,成员取并集)
197
+ */
198
+ public func addPermissionSet(name: String, members: ArrayList<String>): Unit {
199
+ if (!this.permissionSets.contains(name)) {
200
+ this.permissionSets.add(name, HashSet<String>())
201
+ }
202
+ let set = this.permissionSets.get(name).getOrThrow()
203
+ for (m in members) {
204
+ set.add(m)
205
+ }
206
+ }
207
+
208
+ public func hasPermissionSet(name: String): Bool {
209
+ return this.permissionSets.contains(name)
210
+ }
211
+
212
+ /**
213
+ * 清单里引用过、但可能没被任何插件提供的集名(去重)。
214
+ * 供启动提示用:未知集名只是不生效,不是错误——但多半是拼错了。
215
+ */
216
+ public func declaredPermissionSets(): ArrayList<String> {
217
+ let seen = HashSet<String>()
218
+ let names = ArrayList<String>()
219
+ for (c in this.caps) {
220
+ for (n in c.permissions) {
221
+ if (!seen.contains(n)) {
222
+ seen.add(n)
223
+ names.add(n)
224
+ }
225
+ }
226
+ }
227
+ return names
228
+ }
229
+
230
+ /**
231
+ * name 是否被「某条清单引用的某个集」覆盖。
232
+ * 单层展开:集成员若又写成别的集名,不会被继续展开(避免环)。
233
+ */
234
+ private func coveredByPermissionSet(name: String): Bool {
235
+ for (c in this.caps) {
236
+ for (setName in c.permissions) {
237
+ match (this.permissionSets.get(setName)) {
238
+ case Some(members) =>
239
+ if (members.contains(name)) {
240
+ return true
241
+ }
242
+ case None => ()
243
+ }
244
+ }
245
+ }
246
+ return false
247
+ }
248
+
249
+ public func add(cap: Capability): Unit {
250
+ this.caps.add(cap)
251
+ }
252
+
253
+ public func addFromJson(json: JsonValue): Bool {
254
+ if (let Some(cap) <- Capability.fromJson(json)) {
255
+ this.add(cap)
256
+ return true
257
+ }
258
+ return false
259
+ }
260
+
261
+ public func canInvoke(cmd: String): Bool {
262
+ for (c in this.caps) {
263
+ if (c.canInvoke(cmd)) {
264
+ return true
265
+ }
266
+ }
267
+ // 命名权限集算放行:集内成员与写明文同权(v1.1)
268
+ return this.coveredByPermissionSet(cmd)
269
+ }
270
+
271
+ public func canEmit(event: String): Bool {
272
+ for (c in this.caps) {
273
+ if (c.canEmit(event)) {
274
+ return true
275
+ }
276
+ }
277
+ return this.coveredByPermissionSet(event)
278
+ }
279
+
280
+ /**
281
+ * 某个插件的路径 scope(RFC-003 第 4 期):把所有清单里给它的规则**并集**起来。
282
+ * 返回 None = 没有任何清单为它写 scope → 调用方按旧行为全量放行。
283
+ *
284
+ * 为什么是并集而不是「逐清单分别判定」:与 canInvoke / canEmit 的口径一致——
285
+ * 现有能力模型就是「任一清单放行即放行」,scope 跟着同一条口径走才解释得清。
286
+ * (列级 + 窗口级 scopes 是 Tauri v2 更细的模型,本轮不做,见 RFC-003 第 4 期。)
287
+ */
288
+ public func scopeForPlugin(plugin: String): Option<ScopeRule> {
289
+ var found = false
290
+ let merged = ScopeRule()
291
+ for (c in this.caps) {
292
+ match (c.scopes.get(plugin)) {
293
+ case Some(rule) =>
294
+ found = true
295
+ for (a in rule.allow) {
296
+ merged.allow.add(a)
297
+ }
298
+ for (d in rule.deny) {
299
+ merged.deny.add(d)
300
+ }
301
+ case None => ()
302
+ }
303
+ }
304
+ if (!found) {
305
+ return None
306
+ }
307
+ // 运行期叠加的白名单与清单成员同权(放在这里,是为了守住「注册中心是唯一判定口径」——
308
+ // 各插件的 scope 判定全都经 scopeForPlugin,不必各自再查一遍运行期表)。
309
+ this.mergeRuntimeAllow(plugin, merged)
310
+ return Some(merged)
311
+ }
312
+
313
+ /**
314
+ * 运行期给某插件的 scope 追加一条 allow 模式(插件名如 "http",模式写法同 `scope.cj`)。
315
+ *
316
+ * 用途:应用**取得用户明确确认后**(如弹原生确认框)放行一个清单里没有的主机,让本次会话当场可用。
317
+ * 判定顺序不变:**清单里的 deny 命中即拒,运行期加白掀不掉它**。
318
+ *
319
+ * **只收「具体值」,不收通配**:模式里出现 `*` 或 `?` 一律拒绝(返回 false + 一行 stderr)。
320
+ * 加白的语义是「往已有白名单里加一条」,而模式是页面可控字符串——一个通配透传进来就等价于全放行,
321
+ * 整条防线当场作废。这条不变量放在注册中心里强制,任何应用都绕不过去(D-021 的代价第 1 条)。
322
+ */
323
+ public func addRuntimeScopeAllow(plugin: String, pattern: String): Bool {
324
+ if (!isConcreteScopeValue(pattern)) {
325
+ eprintln("[cj-tauri] runtime scope allow refused (${plugin}): pattern must be a concrete value, got '${pattern}'")
326
+ return false
327
+ }
328
+ synchronized(this.scopeLock) {
329
+ if (!this.runtimeScopeAllow.contains(plugin)) {
330
+ this.runtimeScopeAllow.add(plugin, ArrayList<String>())
331
+ }
332
+ let list = this.runtimeScopeAllow.get(plugin).getOrThrow()
333
+ if (!list.contains(pattern)) {
334
+ list.add(pattern)
335
+ }
336
+ }
337
+ return true
338
+ }
339
+
340
+ /**
341
+ * 运行期加白只收「不含通配的具体值」:`*` / `?` 会让模式退化成「全放行」,制表 / 换行 / 回车
342
+ * 则几乎总是「从文件或页面读回来时带了脏字符」,两类都拒(口径见 `addRuntimeScopeAllow`)。
343
+ * 注意 `String` 迭代出来的是 UTF-8 **字节**而不是码点,这里判的正是字节字面量。
344
+ */
345
+ private func isConcreteScopeValue(pattern: String): Bool {
346
+ if (pattern.size == 0) {
347
+ return false
348
+ }
349
+ for (b in pattern) {
350
+ if (b == 42u8 || b == 63u8 || b == 9u8 || b == 10u8 || b == 13u8) {
351
+ return false
352
+ }
353
+ }
354
+ return true
355
+ }
356
+
357
+ /** 某插件的 scope 此刻是否放行某个值(清单 + 运行期叠加;没写 scope 的插件恒放行=旧行为) */
358
+ public func scopeAllowsValue(plugin: String, value: String): Bool {
359
+ match (this.scopeForPlugin(plugin)) {
360
+ case Some(rule) => return scopeAllows(rule, value)
361
+ case None => return true
362
+ }
363
+ }
364
+
365
+ /**
366
+ * 把运行期叠加项并进合并结果。**只有清单已为该插件写过 scope 时才并**:
367
+ * 没写 scope 的插件本来是全量放行(scopeForPlugin 返 None,调用方走旧行为),
368
+ * 这时把运行期条目并进去会把它意外收窄成「只放行这一条」。
369
+ */
370
+ private func mergeRuntimeAllow(plugin: String, merged: ScopeRule): Unit {
371
+ synchronized(this.scopeLock) {
372
+ match (this.runtimeScopeAllow.get(plugin)) {
373
+ case Some(extra) =>
374
+ for (a in extra) {
375
+ merged.allow.add(a)
376
+ }
377
+ case None => ()
378
+ }
379
+ }
380
+ }
381
+
382
+ /** 是否有任何清单为某插件声明过 scope(启动提示与判定分支共用这一个口径) */
383
+ public func hasScopeForPlugin(plugin: String): Bool {
384
+ for (c in this.caps) {
385
+ if (c.scopes.contains(plugin)) {
386
+ return true
387
+ }
388
+ }
389
+ return false
390
+ }
391
+
392
+ public func size(): Int64 {
393
+ return this.caps.size
394
+ }
395
+ }
396
+
397
+ /**
398
+ * 提示清单里引用了、但没有任何插件提供的命名权限集。
399
+ * 只提示不改行为:未知集名不生效(默认最小权限照旧),其它命令与事件不受影响。
400
+ * 调用时机:插件装配完成之后(那时能提供的集才都注册完)。
401
+ */
402
+ public func warnUnknownPermissionSets(reg: CapabilityRegistry): Unit {
403
+ for (name in reg.declaredPermissionSets()) {
404
+ if (!reg.hasPermissionSet(name)) {
405
+ eprintln("[cj-tauri] 未知的命名权限集 \"${name}\":没有插件提供它,这一条不生效")
406
+ eprintln("[cj-tauri] 检查拼写;若它来自插件,确认提供它的插件已 .plugin(...) 装配")
407
+ }
408
+ }
409
+ }