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.
- package/README.md +113 -99
- package/bin/cj-tauri.js +287 -287
- package/cjpm.toml +63 -52
- package/cli/cjpm.toml +22 -22
- package/cli/src/doctor.cj +308 -306
- package/cli/src/main.cj +129 -129
- package/cli/src/manifest.cj +632 -558
- package/cli/src/manifest_test.cj +87 -0
- package/cli/src/project.cj +770 -770
- package/cli/src/resolve.cj +348 -348
- package/cli/src/scaffold.cj +369 -369
- package/cli/src/sync.cj +526 -526
- package/cli/src/util.cj +168 -168
- package/cli/templates/app/README.md +51 -48
- package/cli/templates/app/assets/capabilities/default.json +7 -7
- package/cli/templates/app/assets/ui/index.html +120 -118
- package/cli/templates/app/cj-tauri.toml +32 -22
- package/cli/templates/app/cjpm.toml +34 -34
- package/cli/templates/app/src/main.cj +101 -90
- package/cli/templates/app-react/README.md +56 -56
- package/cli/templates/app-react/assets/capabilities/default.json +7 -7
- package/cli/templates/app-react/assets/ui/index.html +25 -25
- package/cli/templates/app-react/assets/ui/package.json +19 -19
- package/cli/templates/app-react/assets/ui/src/App.css +67 -67
- package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
- package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
- package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
- package/cli/templates/app-react/cj-tauri.toml +33 -23
- package/cli/templates/app-react/cjpm.toml +34 -34
- package/cli/templates/app-react/src/main.cj +91 -91
- package/cli/templates/app-vue/README.md +56 -56
- package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
- package/cli/templates/app-vue/assets/ui/index.html +25 -25
- package/cli/templates/app-vue/assets/ui/package.json +18 -18
- package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
- package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
- package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
- package/cli/templates/app-vue/cj-tauri.toml +33 -23
- package/cli/templates/app-vue/cjpm.toml +34 -34
- package/cli/templates/app-vue/src/main.cj +91 -91
- package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
- package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
- package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
- package/cli/templates/ohos-skeleton/README.md +84 -79
- package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
- package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
- package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
- package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
- package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
- package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
- package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
- package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
- package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
- package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
- package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
- package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
- package/native/build_win.bat +62 -62
- package/native/core/bridge_core.c +1681 -1626
- package/native/core/bridge_core.h +675 -618
- package/native/core/bridge_js.h +108 -108
- package/native/platforms/harmonyos/README.md +115 -6
- package/native/platforms/linux/bridge_linux.c +2787 -2485
- package/native/platforms/windows/bridge_win.c +3740 -3248
- package/native/tests/gtk_threading_probe.c +143 -143
- package/native/tests/test_bridge_core.c +1339 -1228
- package/package.json +53 -53
- package/prebuilt/linux-x64/cj-tauri +0 -0
- package/src/api_system.cj +67 -67
- package/src/app.cj +325 -969
- package/src/app_events.cj +166 -0
- package/src/app_host_ops.cj +287 -0
- package/src/app_run.cj +283 -0
- package/src/app_spec.cj +76 -76
- package/src/arkweb_menu.cj +216 -0
- package/src/arkweb_script.cj +142 -142
- package/src/capability.cj +409 -409
- package/src/clipboard_bridge.cj +178 -0
- package/src/dialog_spec.cj +79 -0
- package/src/dragdrop.cj +26 -26
- package/src/embedded_assets.cj +98 -98
- package/src/host.cj +752 -607
- package/src/host_arkweb.cj +924 -778
- package/src/host_webkit.cj +663 -570
- package/src/host_webview2.cj +663 -570
- package/src/hotkey.cj +66 -66
- package/src/ipc_hub.cj +337 -337
- package/src/ipc_message.cj +286 -286
- package/src/launch.cj +62 -62
- package/src/logging.cj +127 -127
- package/src/menu.cj +197 -197
- package/src/platform/platform_app_context.cj +77 -77
- package/src/platform/platform_assets.cj +33 -33
- package/src/platform/platform_discover.cj +176 -176
- package/src/platform/platform_env.cj +29 -29
- package/src/platform/platform_feature.cj +31 -31
- package/src/platform/platform_page_source.cj +87 -87
- package/src/platform/platform_target_info.cj +32 -32
- package/src/platform_host_provider.cj +87 -87
- package/src/plugin.cj +333 -333
- package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
- package/src/plugins/dialog/plugin_dialog.cj +209 -209
- package/src/plugins/fs/plugin_fs.cj +105 -1
- package/src/plugins/http/http_backend.cj +131 -0
- package/src/plugins/http/http_desktop.cj +195 -0
- package/src/plugins/http/http_ohos.cj +204 -0
- package/src/plugins/http/plugin_http.cj +310 -459
- package/src/plugins/log/plugin_log.cj +105 -105
- package/src/plugins/menu/plugin_menu.cj +158 -158
- package/src/plugins/notification/plugin_notification.cj +154 -154
- package/src/plugins/serial/plugin_serial.cj +458 -454
- package/src/plugins/serial/serial_bridge.cj +194 -194
- package/src/plugins/serial/serial_manager_ohos.cj +92 -0
- package/src/plugins/serial/serial_ohos.cj +529 -0
- package/src/plugins/shell/plugin_shell.cj +258 -258
- package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
- package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
- package/src/plugins/ws/plugin_ws.cj +488 -470
- package/src/process_hidden.cj +174 -174
- package/src/scope.cj +391 -391
- package/src/shell_event.cj +69 -69
- package/src/tests/app_spec_test.cj +126 -127
- package/src/tests/capability_test.cj +243 -243
- package/src/tests/dialog_spec_test.cj +73 -0
- package/src/tests/fake_host.cj +201 -201
- package/src/tests/ipc_hub_test.cj +487 -487
- package/src/tests/logging_test.cj +62 -62
- package/src/tests/platform_contract_test.cj +251 -251
- package/src/tests/plugin_clipboard_test.cj +222 -222
- package/src/tests/plugin_dialog_test.cj +237 -237
- package/src/tests/plugin_fs_test.cj +207 -0
- package/src/tests/plugin_http_test.cj +124 -124
- package/src/tests/plugin_notification_test.cj +205 -205
- package/src/tests/plugin_serial_test.cj +48 -0
- package/src/tests/plugin_test.cj +605 -585
- package/src/tests/plugin_ws_test.cj +40 -40
- package/src/tests/process_hidden_test.cj +129 -129
- package/src/tests/scope_test.cj +350 -350
- package/src/tests/system_host_test.cj +99 -81
- package/src/tests/urlutil_test.cj +53 -53
- package/src/tests/version_test.cj +75 -75
- package/src/tests/window_registry_test.cj +123 -123
- package/src/tray.cj +90 -90
- package/src/urlutil.cj +159 -159
- package/src/version.cj +44 -44
- package/src/window.cj +51 -51
- package/src/window_registry.cj +116 -116
- package/prebuilt/win32-x64/cj-tauri.exe +0 -0
- package/src/plugins/serial/serial_ohos_stub.cj +0 -61
- package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
package/src/scope.cj
CHANGED
|
@@ -1,391 +1,391 @@
|
|
|
1
|
-
package cjTauri
|
|
2
|
-
|
|
3
|
-
import std.collection.*
|
|
4
|
-
import std.env.*
|
|
5
|
-
import std.fs.*
|
|
6
|
-
import stdx.encoding.json.*
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 路径 scope:把「某插件的路径参数能落在哪里」写成白名单(RFC-003 第 4 期)。
|
|
10
|
-
*
|
|
11
|
-
* 为什么放在框架共享层、而不是留给插件自己写:展开变量、规范化、glob 匹配、deny 优先
|
|
12
|
-
* 这几件事只要有第二份实现,就一定会有一份是错的——安全校验不吃「各插件各写一套」。
|
|
13
|
-
* 插件只调 `IpcContext.resolveScopedPath("<插件名>", 页面给的路径)` 取值。
|
|
14
|
-
*
|
|
15
|
-
* 清单写法(capabilities 目录的 json,键是插件名):
|
|
16
|
-
* "scopes": { "fs": { "allow": ["$HOME/notes/**"], "deny": ["**/id_rsa"] } }
|
|
17
|
-
*
|
|
18
|
-
* 语义:完全没有该插件的条目 = 旧行为(全量放行,启动时由 warnUnscopedPlugins 提示);
|
|
19
|
-
* 有条目就按「先 deny 后 allow」判定——allow 必须显式命中,写了空 allow 等于该插件路径全拒。
|
|
20
|
-
*/
|
|
21
|
-
public class ScopeRule {
|
|
22
|
-
public let allow: ArrayList<String>
|
|
23
|
-
public let deny: ArrayList<String>
|
|
24
|
-
|
|
25
|
-
public init() {
|
|
26
|
-
this.allow = ArrayList<String>()
|
|
27
|
-
this.deny = ArrayList<String>()
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
public func isEmpty(): Bool {
|
|
31
|
-
return this.allow.size == 0 && this.deny.size == 0
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 解析一条 scope 规则:`{ "allow": [...], "deny": [...] }`。
|
|
37
|
-
* 数组里非字符串的元素忽略;模式在解析时就展开路径变量(此后只管匹配)。
|
|
38
|
-
*/
|
|
39
|
-
public func parseScopeRule(json: JsonValue): Option<ScopeRule> {
|
|
40
|
-
match (json.kind()) {
|
|
41
|
-
case JsonKind.JsObject => ()
|
|
42
|
-
case _ => return None
|
|
43
|
-
}
|
|
44
|
-
let obj = json.asObject()
|
|
45
|
-
let rule = ScopeRule()
|
|
46
|
-
// 模式与页面路径走同一套分隔符口径:$HOME 在 Windows 展开出的是 USERPROFILE 原值
|
|
47
|
-
// (C:\Users\...,反斜杠),不归一的话拿它匹配 normalizeScopePath 的正斜杠结果永远不中
|
|
48
|
-
// ——`$HOME/**` 在 Windows 上形同虚设(实测 Desktop 文件被拒)。
|
|
49
|
-
if (let Some(a) <- obj.get("allow")) {
|
|
50
|
-
collectScopePatterns(a, rule.allow)
|
|
51
|
-
}
|
|
52
|
-
if (let Some(d) <- obj.get("deny")) {
|
|
53
|
-
collectScopePatterns(d, rule.deny)
|
|
54
|
-
}
|
|
55
|
-
return Some(rule)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
private func collectScopePatterns(v: JsonValue, out: ArrayList<String>): Unit {
|
|
59
|
-
match (v.kind()) {
|
|
60
|
-
case JsonKind.JsArray =>
|
|
61
|
-
let arr = v.asArray()
|
|
62
|
-
for (i in 0..arr.size()) {
|
|
63
|
-
if (let Some(x) <- arr.get(i)) {
|
|
64
|
-
match (x.kind()) {
|
|
65
|
-
case JsonKind.JsString =>
|
|
66
|
-
out.add(toScopeSlashes(expandScopeVars(x.asString().getValue())))
|
|
67
|
-
case _ => ()
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// 单条字符串也收:写 "fs": { "allow": "$HOME/**" } 与写成数组等价,少一处踩坑
|
|
72
|
-
case JsonKind.JsString =>
|
|
73
|
-
out.add(toScopeSlashes(expandScopeVars(v.asString().getValue())))
|
|
74
|
-
case _ => ()
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* 判定:路径是否被这条 scope 放行。
|
|
80
|
-
* 顺序是先 deny 后 allow——命中 deny 一律拒(哪怕同时命中 allow);allow 必须显式命中。
|
|
81
|
-
*/
|
|
82
|
-
public func scopeAllows(rule: ScopeRule, normalizedPath: String): Bool {
|
|
83
|
-
for (d in rule.deny) {
|
|
84
|
-
if (globMatch(d, normalizedPath)) {
|
|
85
|
-
return false
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
for (a in rule.allow) {
|
|
89
|
-
if (globMatch(a, normalizedPath)) {
|
|
90
|
-
return true
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return false
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* glob 匹配:`*` 不跨路径分隔符、`**` 跨分隔符、`?` 匹配**一个码点**(不是一字节——
|
|
98
|
-
* 汉字是 3 字节,按字节吃掉一个会把它切碎)。
|
|
99
|
-
*
|
|
100
|
-
* 按 UTF-8 字节比较(非 ASCII 字节 >= 0x80,与所有 ASCII 元字符都不撞),
|
|
101
|
-
* 所以 `?` 要多吃掉后续的续接字节(0x80-0xBF)才算一个码点。
|
|
102
|
-
*/
|
|
103
|
-
public func globMatch(pattern: String, target: String): Bool {
|
|
104
|
-
return globMatchAt(foldCase(pattern).toArray(), 0, foldCase(target).toArray(), 0)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** 仅 Windows 折叠大小写(文件系统不区分大小写);Linux 保持敏感 */
|
|
108
|
-
@When[os == "Windows"]
|
|
109
|
-
func foldCase(s: String): String {
|
|
110
|
-
return s.toAsciiLower()
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
@When[os == "Linux"]
|
|
114
|
-
func foldCase(s: String): String {
|
|
115
|
-
return s
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
private func isContinuationByte(b: UInt8): Bool {
|
|
119
|
-
return (b & 0xC0u8) == 0x80u8
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private func globMatchAt(p: Array<UInt8>, pi: Int64, t: Array<UInt8>, ti: Int64): Bool {
|
|
123
|
-
var i = pi
|
|
124
|
-
var j = ti
|
|
125
|
-
while (i < p.size) {
|
|
126
|
-
let pc = p[i]
|
|
127
|
-
if (pc == 42u8) {
|
|
128
|
-
// 星号(可能连写 = 跨分隔符的 `**`)
|
|
129
|
-
var k = i
|
|
130
|
-
while (k < p.size && p[k] == 42u8) {
|
|
131
|
-
k++
|
|
132
|
-
}
|
|
133
|
-
let crossSlash = (k - i) >= 2
|
|
134
|
-
if (k >= p.size) {
|
|
135
|
-
// 模式尾部是星号
|
|
136
|
-
if (crossSlash) {
|
|
137
|
-
return true
|
|
138
|
-
}
|
|
139
|
-
var m = j
|
|
140
|
-
while (m < t.size) {
|
|
141
|
-
if (t[m] == 47u8) {
|
|
142
|
-
return false
|
|
143
|
-
}
|
|
144
|
-
m++
|
|
145
|
-
}
|
|
146
|
-
return true
|
|
147
|
-
}
|
|
148
|
-
var m = j
|
|
149
|
-
while (m <= t.size) {
|
|
150
|
-
if (globMatchAt(p, k, t, m)) {
|
|
151
|
-
return true
|
|
152
|
-
}
|
|
153
|
-
if (m >= t.size) {
|
|
154
|
-
break
|
|
155
|
-
}
|
|
156
|
-
if (!crossSlash && t[m] == 47u8) {
|
|
157
|
-
break
|
|
158
|
-
}
|
|
159
|
-
m++
|
|
160
|
-
}
|
|
161
|
-
// `**/` 之后那个斜杠是可选的:`/a/**/b` 要能匹配 `/a/b`(零个目录)
|
|
162
|
-
if (crossSlash && p[k] == 47u8) {
|
|
163
|
-
return globMatchAt(p, k + 1, t, j)
|
|
164
|
-
}
|
|
165
|
-
return false
|
|
166
|
-
}
|
|
167
|
-
if (pc == 63u8) {
|
|
168
|
-
// '?':一个码点,且不吃分隔符
|
|
169
|
-
if (j >= t.size || t[j] == 47u8) {
|
|
170
|
-
return false
|
|
171
|
-
}
|
|
172
|
-
j++
|
|
173
|
-
while (j < t.size && isContinuationByte(t[j])) {
|
|
174
|
-
j++
|
|
175
|
-
}
|
|
176
|
-
i++
|
|
177
|
-
continue
|
|
178
|
-
}
|
|
179
|
-
if (j >= t.size || t[j] != pc) {
|
|
180
|
-
return false
|
|
181
|
-
}
|
|
182
|
-
i++
|
|
183
|
-
j++
|
|
184
|
-
}
|
|
185
|
-
return j == t.size
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/* ===== 路径处理 ===== */
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* 工作目录的惰性缓存(顶层只放一个 `None` 常量,真正的解析推迟到首次用到 `$CWD`)
|
|
192
|
-
*
|
|
193
|
-
* **为什么不能写成 `let cache = ScopeCwdCache()`**:鸿蒙上框架包由 `InitCJLibrary`
|
|
194
|
-
* 在动态库加载期执行包全局初始化(`cjTauri::package_global_init`),那一刻仓颉堆还没起来;
|
|
195
|
-
* 任何在初始化期做分配的顶层构造器都会让应用当场 SEGV——实测 2026-10-19:
|
|
196
|
-
* 崩溃落在 `[anon:cangjie_heap]`(SEGV_ACCERR / SEGV_MAPERR),调用链是
|
|
197
|
-
* `InitCJLibrary → cangjie_entry::package_global_init → cjTauri::package_global_init`,
|
|
198
|
-
* 窗口起不来、hilog 里连一行我方日志都没有。桌面两平台是静态全链、初始化期建对象没事,
|
|
199
|
-
* 所以这个坑只在鸿蒙的多动态库形态下暴露。
|
|
200
|
-
*/
|
|
201
|
-
var scopeCwdCache: Option<String> = None
|
|
202
|
-
|
|
203
|
-
/** 进程工作目录(项目根;`canonicalize(".")` 顺带解决绝对化与软链接) */
|
|
204
|
-
public func scopeCwdPath(): String {
|
|
205
|
-
match (scopeCwdCache) {
|
|
206
|
-
case Some(v) => return v
|
|
207
|
-
case None => ()
|
|
208
|
-
}
|
|
209
|
-
var value = "."
|
|
210
|
-
try {
|
|
211
|
-
value = toScopeSlashes(canonicalize(".").toString())
|
|
212
|
-
} catch (e: Exception) {
|
|
213
|
-
let _ = e
|
|
214
|
-
}
|
|
215
|
-
scopeCwdCache = Some(value)
|
|
216
|
-
return value
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* 清单模式与目标路径**统一**的反斜杠归一(两平台同一口径,d8014ad 的回归用例锁定):
|
|
221
|
-
* Windows 的 $HOME 展开出反斜杠路径,不归一就拿它匹配不上正斜杠目标;
|
|
222
|
-
* Linux 侧同样归一——清单是应用作者写的(可能照 Windows 风格写 $CWD\notes),
|
|
223
|
-
* 模式与目标走同一个函数才不会「一边归一了一边没归」造成永远匹配不上。
|
|
224
|
-
* Linux 文件名里真含反斜杠属于 exotic 场景,模式与目标同函数归一后行为仍一致。
|
|
225
|
-
*/
|
|
226
|
-
func toScopeSlashes(s: String): String {
|
|
227
|
-
return s.replace("\\", "/")
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* 展开路径变量:`$HOME` / `$TEMP` / `$CWD`。
|
|
232
|
-
*
|
|
233
|
-
* 认不出的变量**原样保留**(连 `$` 一起留在结果里)——于是模式匹配不上 = 拒。
|
|
234
|
-
* 这是有意的失败方向:拼错变量名只会让规则变严,不会偷偷放行。
|
|
235
|
-
*/
|
|
236
|
-
public func expandScopeVars(raw: String): String {
|
|
237
|
-
let bytes = raw.toArray()
|
|
238
|
-
var out = ""
|
|
239
|
-
var seg: Int64 = 0
|
|
240
|
-
var i: Int64 = 0
|
|
241
|
-
while (i < bytes.size) {
|
|
242
|
-
if (bytes[i] != 36u8) { // '$'
|
|
243
|
-
i++
|
|
244
|
-
continue
|
|
245
|
-
}
|
|
246
|
-
var j = i + 1
|
|
247
|
-
while (j < bytes.size && isVarNameByte(bytes[j])) {
|
|
248
|
-
j++
|
|
249
|
-
}
|
|
250
|
-
if (j == i + 1) {
|
|
251
|
-
i++
|
|
252
|
-
continue
|
|
253
|
-
}
|
|
254
|
-
out += raw[seg..i]
|
|
255
|
-
let name = raw[(i + 1)..j]
|
|
256
|
-
match (scopeVarValue(name)) {
|
|
257
|
-
case Some(v) => out += v
|
|
258
|
-
case None => out += raw[i..j]
|
|
259
|
-
}
|
|
260
|
-
seg = j
|
|
261
|
-
i = j
|
|
262
|
-
}
|
|
263
|
-
out += raw[seg..raw.size]
|
|
264
|
-
return out
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
private func isVarNameByte(b: UInt8): Bool {
|
|
268
|
-
if (b >= 97u8 && b <= 122u8) {
|
|
269
|
-
return true
|
|
270
|
-
}
|
|
271
|
-
if (b >= 65u8 && b <= 90u8) {
|
|
272
|
-
return true
|
|
273
|
-
}
|
|
274
|
-
if (b >= 48u8 && b <= 57u8) {
|
|
275
|
-
return true
|
|
276
|
-
}
|
|
277
|
-
return b == 95u8
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
func scopeVarValue(name: String): Option<String> {
|
|
281
|
-
if (name == "HOME") {
|
|
282
|
-
return firstEnvOf(["HOME", "USERPROFILE"])
|
|
283
|
-
}
|
|
284
|
-
if (name == "TEMP") {
|
|
285
|
-
return firstEnvOf(["TEMP", "TMPDIR"])
|
|
286
|
-
}
|
|
287
|
-
if (name == "CWD") {
|
|
288
|
-
return Some(scopeCwdPath())
|
|
289
|
-
}
|
|
290
|
-
return None
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
func firstEnvOf(names: Array<String>): Option<String> {
|
|
294
|
-
for (n in names) {
|
|
295
|
-
if (let Some(v) <- getVariable(n)) {
|
|
296
|
-
if (v.size > 0) {
|
|
297
|
-
return Some(v)
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
return None
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* 规范化路径(scope 模式与页面给的路径走同一个函数,保证两侧口径一致):
|
|
306
|
-
* 1) 展开变量、统一分隔符;
|
|
307
|
-
* 2) 路径存在 → `std.fs.canonicalize`(绝对化 + 解析软链接;白名单目录里一条指向
|
|
308
|
-
* 别处的软链接也会被照出真身);
|
|
309
|
-
* 3) 不存在(写新文件、或带星号的模式)→ 词法规范化:绝对化 + 折叠 `.` 与 `..`。
|
|
310
|
-
*/
|
|
311
|
-
public func normalizeScopePath(raw: String): String {
|
|
312
|
-
let expanded = toScopeSlashes(expandScopeVars(raw))
|
|
313
|
-
try {
|
|
314
|
-
return toScopeSlashes(canonicalize(expanded).toString())
|
|
315
|
-
} catch (e: Exception) {
|
|
316
|
-
let _ = e
|
|
317
|
-
()
|
|
318
|
-
}
|
|
319
|
-
return lexicalScopePath(expanded)
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
func isScopeAbsolute(p: String): Bool {
|
|
323
|
-
let b = p.toArray()
|
|
324
|
-
if (b.size == 0) {
|
|
325
|
-
return false
|
|
326
|
-
}
|
|
327
|
-
if (b[0] == 47u8) { // '/'
|
|
328
|
-
return true
|
|
329
|
-
}
|
|
330
|
-
// Windows 盘符:"X:/"
|
|
331
|
-
if (b.size >= 3 && b[1] == 58u8 && b[2] == 47u8) {
|
|
332
|
-
return isAsciiAlpha(b[0])
|
|
333
|
-
}
|
|
334
|
-
return false
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
private func isAsciiAlpha(b: UInt8): Bool {
|
|
338
|
-
return (b >= 97u8 && b <= 122u8) || (b >= 65u8 && b <= 90u8)
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/** 文本层面的规范化:不碰文件系统,因此对「还不存在的路径」也成立 */
|
|
342
|
-
func lexicalScopePath(p: String): String {
|
|
343
|
-
let abs = if (isScopeAbsolute(p)) {
|
|
344
|
-
p
|
|
345
|
-
} else {
|
|
346
|
-
scopeCwdPath() + "/" + p
|
|
347
|
-
}
|
|
348
|
-
let bytes = abs.toArray()
|
|
349
|
-
var root = ""
|
|
350
|
-
var body = abs
|
|
351
|
-
if (bytes.size > 0 && bytes[0] == 47u8) {
|
|
352
|
-
root = "/"
|
|
353
|
-
body = abs[1..abs.size]
|
|
354
|
-
} else if (bytes.size >= 3 && bytes[1] == 58u8 && isAsciiAlpha(bytes[0])) {
|
|
355
|
-
root = abs[0..2] // "X:"
|
|
356
|
-
body = abs[2..abs.size]
|
|
357
|
-
}
|
|
358
|
-
let b2 = body.toArray()
|
|
359
|
-
let segs = ArrayList<String>()
|
|
360
|
-
var start: Int64 = 0
|
|
361
|
-
var i: Int64 = 0
|
|
362
|
-
while (i <= b2.size) {
|
|
363
|
-
if (i == b2.size || b2[i] == 47u8) {
|
|
364
|
-
let seg = body[start..i]
|
|
365
|
-
if (seg == "..") {
|
|
366
|
-
if (segs.size > 0) {
|
|
367
|
-
segs.remove(at: segs.size - 1)
|
|
368
|
-
}
|
|
369
|
-
} else if (seg != "." && seg.size > 0) {
|
|
370
|
-
segs.add(seg)
|
|
371
|
-
}
|
|
372
|
-
start = i + 1
|
|
373
|
-
}
|
|
374
|
-
i++
|
|
375
|
-
}
|
|
376
|
-
var out = root
|
|
377
|
-
for (s in segs) {
|
|
378
|
-
if (out.size == 0 || out == "/") {
|
|
379
|
-
out += s
|
|
380
|
-
} else {
|
|
381
|
-
out += "/" + s
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
if (out.size == 0) {
|
|
385
|
-
if (root.size > 0) {
|
|
386
|
-
return root
|
|
387
|
-
}
|
|
388
|
-
return "/"
|
|
389
|
-
}
|
|
390
|
-
return out
|
|
391
|
-
}
|
|
1
|
+
package cjTauri
|
|
2
|
+
|
|
3
|
+
import std.collection.*
|
|
4
|
+
import std.env.*
|
|
5
|
+
import std.fs.*
|
|
6
|
+
import stdx.encoding.json.*
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 路径 scope:把「某插件的路径参数能落在哪里」写成白名单(RFC-003 第 4 期)。
|
|
10
|
+
*
|
|
11
|
+
* 为什么放在框架共享层、而不是留给插件自己写:展开变量、规范化、glob 匹配、deny 优先
|
|
12
|
+
* 这几件事只要有第二份实现,就一定会有一份是错的——安全校验不吃「各插件各写一套」。
|
|
13
|
+
* 插件只调 `IpcContext.resolveScopedPath("<插件名>", 页面给的路径)` 取值。
|
|
14
|
+
*
|
|
15
|
+
* 清单写法(capabilities 目录的 json,键是插件名):
|
|
16
|
+
* "scopes": { "fs": { "allow": ["$HOME/notes/**"], "deny": ["**/id_rsa"] } }
|
|
17
|
+
*
|
|
18
|
+
* 语义:完全没有该插件的条目 = 旧行为(全量放行,启动时由 warnUnscopedPlugins 提示);
|
|
19
|
+
* 有条目就按「先 deny 后 allow」判定——allow 必须显式命中,写了空 allow 等于该插件路径全拒。
|
|
20
|
+
*/
|
|
21
|
+
public class ScopeRule {
|
|
22
|
+
public let allow: ArrayList<String>
|
|
23
|
+
public let deny: ArrayList<String>
|
|
24
|
+
|
|
25
|
+
public init() {
|
|
26
|
+
this.allow = ArrayList<String>()
|
|
27
|
+
this.deny = ArrayList<String>()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public func isEmpty(): Bool {
|
|
31
|
+
return this.allow.size == 0 && this.deny.size == 0
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 解析一条 scope 规则:`{ "allow": [...], "deny": [...] }`。
|
|
37
|
+
* 数组里非字符串的元素忽略;模式在解析时就展开路径变量(此后只管匹配)。
|
|
38
|
+
*/
|
|
39
|
+
public func parseScopeRule(json: JsonValue): Option<ScopeRule> {
|
|
40
|
+
match (json.kind()) {
|
|
41
|
+
case JsonKind.JsObject => ()
|
|
42
|
+
case _ => return None
|
|
43
|
+
}
|
|
44
|
+
let obj = json.asObject()
|
|
45
|
+
let rule = ScopeRule()
|
|
46
|
+
// 模式与页面路径走同一套分隔符口径:$HOME 在 Windows 展开出的是 USERPROFILE 原值
|
|
47
|
+
// (C:\Users\...,反斜杠),不归一的话拿它匹配 normalizeScopePath 的正斜杠结果永远不中
|
|
48
|
+
// ——`$HOME/**` 在 Windows 上形同虚设(实测 Desktop 文件被拒)。
|
|
49
|
+
if (let Some(a) <- obj.get("allow")) {
|
|
50
|
+
collectScopePatterns(a, rule.allow)
|
|
51
|
+
}
|
|
52
|
+
if (let Some(d) <- obj.get("deny")) {
|
|
53
|
+
collectScopePatterns(d, rule.deny)
|
|
54
|
+
}
|
|
55
|
+
return Some(rule)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private func collectScopePatterns(v: JsonValue, out: ArrayList<String>): Unit {
|
|
59
|
+
match (v.kind()) {
|
|
60
|
+
case JsonKind.JsArray =>
|
|
61
|
+
let arr = v.asArray()
|
|
62
|
+
for (i in 0..arr.size()) {
|
|
63
|
+
if (let Some(x) <- arr.get(i)) {
|
|
64
|
+
match (x.kind()) {
|
|
65
|
+
case JsonKind.JsString =>
|
|
66
|
+
out.add(toScopeSlashes(expandScopeVars(x.asString().getValue())))
|
|
67
|
+
case _ => ()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// 单条字符串也收:写 "fs": { "allow": "$HOME/**" } 与写成数组等价,少一处踩坑
|
|
72
|
+
case JsonKind.JsString =>
|
|
73
|
+
out.add(toScopeSlashes(expandScopeVars(v.asString().getValue())))
|
|
74
|
+
case _ => ()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 判定:路径是否被这条 scope 放行。
|
|
80
|
+
* 顺序是先 deny 后 allow——命中 deny 一律拒(哪怕同时命中 allow);allow 必须显式命中。
|
|
81
|
+
*/
|
|
82
|
+
public func scopeAllows(rule: ScopeRule, normalizedPath: String): Bool {
|
|
83
|
+
for (d in rule.deny) {
|
|
84
|
+
if (globMatch(d, normalizedPath)) {
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (a in rule.allow) {
|
|
89
|
+
if (globMatch(a, normalizedPath)) {
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* glob 匹配:`*` 不跨路径分隔符、`**` 跨分隔符、`?` 匹配**一个码点**(不是一字节——
|
|
98
|
+
* 汉字是 3 字节,按字节吃掉一个会把它切碎)。
|
|
99
|
+
*
|
|
100
|
+
* 按 UTF-8 字节比较(非 ASCII 字节 >= 0x80,与所有 ASCII 元字符都不撞),
|
|
101
|
+
* 所以 `?` 要多吃掉后续的续接字节(0x80-0xBF)才算一个码点。
|
|
102
|
+
*/
|
|
103
|
+
public func globMatch(pattern: String, target: String): Bool {
|
|
104
|
+
return globMatchAt(foldCase(pattern).toArray(), 0, foldCase(target).toArray(), 0)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 仅 Windows 折叠大小写(文件系统不区分大小写);Linux 保持敏感 */
|
|
108
|
+
@When[os == "Windows"]
|
|
109
|
+
func foldCase(s: String): String {
|
|
110
|
+
return s.toAsciiLower()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@When[os == "Linux"]
|
|
114
|
+
func foldCase(s: String): String {
|
|
115
|
+
return s
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private func isContinuationByte(b: UInt8): Bool {
|
|
119
|
+
return (b & 0xC0u8) == 0x80u8
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private func globMatchAt(p: Array<UInt8>, pi: Int64, t: Array<UInt8>, ti: Int64): Bool {
|
|
123
|
+
var i = pi
|
|
124
|
+
var j = ti
|
|
125
|
+
while (i < p.size) {
|
|
126
|
+
let pc = p[i]
|
|
127
|
+
if (pc == 42u8) {
|
|
128
|
+
// 星号(可能连写 = 跨分隔符的 `**`)
|
|
129
|
+
var k = i
|
|
130
|
+
while (k < p.size && p[k] == 42u8) {
|
|
131
|
+
k++
|
|
132
|
+
}
|
|
133
|
+
let crossSlash = (k - i) >= 2
|
|
134
|
+
if (k >= p.size) {
|
|
135
|
+
// 模式尾部是星号
|
|
136
|
+
if (crossSlash) {
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
139
|
+
var m = j
|
|
140
|
+
while (m < t.size) {
|
|
141
|
+
if (t[m] == 47u8) {
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
m++
|
|
145
|
+
}
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
var m = j
|
|
149
|
+
while (m <= t.size) {
|
|
150
|
+
if (globMatchAt(p, k, t, m)) {
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
if (m >= t.size) {
|
|
154
|
+
break
|
|
155
|
+
}
|
|
156
|
+
if (!crossSlash && t[m] == 47u8) {
|
|
157
|
+
break
|
|
158
|
+
}
|
|
159
|
+
m++
|
|
160
|
+
}
|
|
161
|
+
// `**/` 之后那个斜杠是可选的:`/a/**/b` 要能匹配 `/a/b`(零个目录)
|
|
162
|
+
if (crossSlash && p[k] == 47u8) {
|
|
163
|
+
return globMatchAt(p, k + 1, t, j)
|
|
164
|
+
}
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
if (pc == 63u8) {
|
|
168
|
+
// '?':一个码点,且不吃分隔符
|
|
169
|
+
if (j >= t.size || t[j] == 47u8) {
|
|
170
|
+
return false
|
|
171
|
+
}
|
|
172
|
+
j++
|
|
173
|
+
while (j < t.size && isContinuationByte(t[j])) {
|
|
174
|
+
j++
|
|
175
|
+
}
|
|
176
|
+
i++
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
if (j >= t.size || t[j] != pc) {
|
|
180
|
+
return false
|
|
181
|
+
}
|
|
182
|
+
i++
|
|
183
|
+
j++
|
|
184
|
+
}
|
|
185
|
+
return j == t.size
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ===== 路径处理 ===== */
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 工作目录的惰性缓存(顶层只放一个 `None` 常量,真正的解析推迟到首次用到 `$CWD`)
|
|
192
|
+
*
|
|
193
|
+
* **为什么不能写成 `let cache = ScopeCwdCache()`**:鸿蒙上框架包由 `InitCJLibrary`
|
|
194
|
+
* 在动态库加载期执行包全局初始化(`cjTauri::package_global_init`),那一刻仓颉堆还没起来;
|
|
195
|
+
* 任何在初始化期做分配的顶层构造器都会让应用当场 SEGV——实测 2026-10-19:
|
|
196
|
+
* 崩溃落在 `[anon:cangjie_heap]`(SEGV_ACCERR / SEGV_MAPERR),调用链是
|
|
197
|
+
* `InitCJLibrary → cangjie_entry::package_global_init → cjTauri::package_global_init`,
|
|
198
|
+
* 窗口起不来、hilog 里连一行我方日志都没有。桌面两平台是静态全链、初始化期建对象没事,
|
|
199
|
+
* 所以这个坑只在鸿蒙的多动态库形态下暴露。
|
|
200
|
+
*/
|
|
201
|
+
var scopeCwdCache: Option<String> = None
|
|
202
|
+
|
|
203
|
+
/** 进程工作目录(项目根;`canonicalize(".")` 顺带解决绝对化与软链接) */
|
|
204
|
+
public func scopeCwdPath(): String {
|
|
205
|
+
match (scopeCwdCache) {
|
|
206
|
+
case Some(v) => return v
|
|
207
|
+
case None => ()
|
|
208
|
+
}
|
|
209
|
+
var value = "."
|
|
210
|
+
try {
|
|
211
|
+
value = toScopeSlashes(canonicalize(".").toString())
|
|
212
|
+
} catch (e: Exception) {
|
|
213
|
+
let _ = e
|
|
214
|
+
}
|
|
215
|
+
scopeCwdCache = Some(value)
|
|
216
|
+
return value
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 清单模式与目标路径**统一**的反斜杠归一(两平台同一口径,d8014ad 的回归用例锁定):
|
|
221
|
+
* Windows 的 $HOME 展开出反斜杠路径,不归一就拿它匹配不上正斜杠目标;
|
|
222
|
+
* Linux 侧同样归一——清单是应用作者写的(可能照 Windows 风格写 $CWD\notes),
|
|
223
|
+
* 模式与目标走同一个函数才不会「一边归一了一边没归」造成永远匹配不上。
|
|
224
|
+
* Linux 文件名里真含反斜杠属于 exotic 场景,模式与目标同函数归一后行为仍一致。
|
|
225
|
+
*/
|
|
226
|
+
func toScopeSlashes(s: String): String {
|
|
227
|
+
return s.replace("\\", "/")
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 展开路径变量:`$HOME` / `$TEMP` / `$CWD`。
|
|
232
|
+
*
|
|
233
|
+
* 认不出的变量**原样保留**(连 `$` 一起留在结果里)——于是模式匹配不上 = 拒。
|
|
234
|
+
* 这是有意的失败方向:拼错变量名只会让规则变严,不会偷偷放行。
|
|
235
|
+
*/
|
|
236
|
+
public func expandScopeVars(raw: String): String {
|
|
237
|
+
let bytes = raw.toArray()
|
|
238
|
+
var out = ""
|
|
239
|
+
var seg: Int64 = 0
|
|
240
|
+
var i: Int64 = 0
|
|
241
|
+
while (i < bytes.size) {
|
|
242
|
+
if (bytes[i] != 36u8) { // '$'
|
|
243
|
+
i++
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
var j = i + 1
|
|
247
|
+
while (j < bytes.size && isVarNameByte(bytes[j])) {
|
|
248
|
+
j++
|
|
249
|
+
}
|
|
250
|
+
if (j == i + 1) {
|
|
251
|
+
i++
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
out += raw[seg..i]
|
|
255
|
+
let name = raw[(i + 1)..j]
|
|
256
|
+
match (scopeVarValue(name)) {
|
|
257
|
+
case Some(v) => out += v
|
|
258
|
+
case None => out += raw[i..j]
|
|
259
|
+
}
|
|
260
|
+
seg = j
|
|
261
|
+
i = j
|
|
262
|
+
}
|
|
263
|
+
out += raw[seg..raw.size]
|
|
264
|
+
return out
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private func isVarNameByte(b: UInt8): Bool {
|
|
268
|
+
if (b >= 97u8 && b <= 122u8) {
|
|
269
|
+
return true
|
|
270
|
+
}
|
|
271
|
+
if (b >= 65u8 && b <= 90u8) {
|
|
272
|
+
return true
|
|
273
|
+
}
|
|
274
|
+
if (b >= 48u8 && b <= 57u8) {
|
|
275
|
+
return true
|
|
276
|
+
}
|
|
277
|
+
return b == 95u8
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
func scopeVarValue(name: String): Option<String> {
|
|
281
|
+
if (name == "HOME") {
|
|
282
|
+
return firstEnvOf(["HOME", "USERPROFILE"])
|
|
283
|
+
}
|
|
284
|
+
if (name == "TEMP") {
|
|
285
|
+
return firstEnvOf(["TEMP", "TMPDIR"])
|
|
286
|
+
}
|
|
287
|
+
if (name == "CWD") {
|
|
288
|
+
return Some(scopeCwdPath())
|
|
289
|
+
}
|
|
290
|
+
return None
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
func firstEnvOf(names: Array<String>): Option<String> {
|
|
294
|
+
for (n in names) {
|
|
295
|
+
if (let Some(v) <- getVariable(n)) {
|
|
296
|
+
if (v.size > 0) {
|
|
297
|
+
return Some(v)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return None
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* 规范化路径(scope 模式与页面给的路径走同一个函数,保证两侧口径一致):
|
|
306
|
+
* 1) 展开变量、统一分隔符;
|
|
307
|
+
* 2) 路径存在 → `std.fs.canonicalize`(绝对化 + 解析软链接;白名单目录里一条指向
|
|
308
|
+
* 别处的软链接也会被照出真身);
|
|
309
|
+
* 3) 不存在(写新文件、或带星号的模式)→ 词法规范化:绝对化 + 折叠 `.` 与 `..`。
|
|
310
|
+
*/
|
|
311
|
+
public func normalizeScopePath(raw: String): String {
|
|
312
|
+
let expanded = toScopeSlashes(expandScopeVars(raw))
|
|
313
|
+
try {
|
|
314
|
+
return toScopeSlashes(canonicalize(expanded).toString())
|
|
315
|
+
} catch (e: Exception) {
|
|
316
|
+
let _ = e
|
|
317
|
+
()
|
|
318
|
+
}
|
|
319
|
+
return lexicalScopePath(expanded)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
func isScopeAbsolute(p: String): Bool {
|
|
323
|
+
let b = p.toArray()
|
|
324
|
+
if (b.size == 0) {
|
|
325
|
+
return false
|
|
326
|
+
}
|
|
327
|
+
if (b[0] == 47u8) { // '/'
|
|
328
|
+
return true
|
|
329
|
+
}
|
|
330
|
+
// Windows 盘符:"X:/"
|
|
331
|
+
if (b.size >= 3 && b[1] == 58u8 && b[2] == 47u8) {
|
|
332
|
+
return isAsciiAlpha(b[0])
|
|
333
|
+
}
|
|
334
|
+
return false
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private func isAsciiAlpha(b: UInt8): Bool {
|
|
338
|
+
return (b >= 97u8 && b <= 122u8) || (b >= 65u8 && b <= 90u8)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** 文本层面的规范化:不碰文件系统,因此对「还不存在的路径」也成立 */
|
|
342
|
+
func lexicalScopePath(p: String): String {
|
|
343
|
+
let abs = if (isScopeAbsolute(p)) {
|
|
344
|
+
p
|
|
345
|
+
} else {
|
|
346
|
+
scopeCwdPath() + "/" + p
|
|
347
|
+
}
|
|
348
|
+
let bytes = abs.toArray()
|
|
349
|
+
var root = ""
|
|
350
|
+
var body = abs
|
|
351
|
+
if (bytes.size > 0 && bytes[0] == 47u8) {
|
|
352
|
+
root = "/"
|
|
353
|
+
body = abs[1..abs.size]
|
|
354
|
+
} else if (bytes.size >= 3 && bytes[1] == 58u8 && isAsciiAlpha(bytes[0])) {
|
|
355
|
+
root = abs[0..2] // "X:"
|
|
356
|
+
body = abs[2..abs.size]
|
|
357
|
+
}
|
|
358
|
+
let b2 = body.toArray()
|
|
359
|
+
let segs = ArrayList<String>()
|
|
360
|
+
var start: Int64 = 0
|
|
361
|
+
var i: Int64 = 0
|
|
362
|
+
while (i <= b2.size) {
|
|
363
|
+
if (i == b2.size || b2[i] == 47u8) {
|
|
364
|
+
let seg = body[start..i]
|
|
365
|
+
if (seg == "..") {
|
|
366
|
+
if (segs.size > 0) {
|
|
367
|
+
segs.remove(at: segs.size - 1)
|
|
368
|
+
}
|
|
369
|
+
} else if (seg != "." && seg.size > 0) {
|
|
370
|
+
segs.add(seg)
|
|
371
|
+
}
|
|
372
|
+
start = i + 1
|
|
373
|
+
}
|
|
374
|
+
i++
|
|
375
|
+
}
|
|
376
|
+
var out = root
|
|
377
|
+
for (s in segs) {
|
|
378
|
+
if (out.size == 0 || out == "/") {
|
|
379
|
+
out += s
|
|
380
|
+
} else {
|
|
381
|
+
out += "/" + s
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (out.size == 0) {
|
|
385
|
+
if (root.size > 0) {
|
|
386
|
+
return root
|
|
387
|
+
}
|
|
388
|
+
return "/"
|
|
389
|
+
}
|
|
390
|
+
return out
|
|
391
|
+
}
|