@yunzai-ng/core 0.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/LICENSE +661 -0
- package/README.md +51 -0
- package/dist/adapter/accounts.d.ts +174 -0
- package/dist/adapter/accounts.d.ts.map +1 -0
- package/dist/adapter/accounts.js +653 -0
- package/dist/adapter/accounts.js.map +1 -0
- package/dist/adapter/bots.d.ts +142 -0
- package/dist/adapter/bots.d.ts.map +1 -0
- package/dist/adapter/bots.js +454 -0
- package/dist/adapter/bots.js.map +1 -0
- package/dist/adapter/host.d.ts +114 -0
- package/dist/adapter/host.d.ts.map +1 -0
- package/dist/adapter/host.js +90 -0
- package/dist/adapter/host.js.map +1 -0
- package/dist/adapter/login.d.ts +174 -0
- package/dist/adapter/login.d.ts.map +1 -0
- package/dist/adapter/login.js +345 -0
- package/dist/adapter/login.js.map +1 -0
- package/dist/adapter/registry.d.ts +102 -0
- package/dist/adapter/registry.d.ts.map +1 -0
- package/dist/adapter/registry.js +150 -0
- package/dist/adapter/registry.js.map +1 -0
- package/dist/config/core-config.d.ts +151 -0
- package/dist/config/core-config.d.ts.map +1 -0
- package/dist/config/core-config.js +338 -0
- package/dist/config/core-config.js.map +1 -0
- package/dist/config/schema.d.ts +449 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +871 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/store.d.ts +184 -0
- package/dist/config/store.d.ts.map +1 -0
- package/dist/config/store.js +428 -0
- package/dist/config/store.js.map +1 -0
- package/dist/config/yaml.d.ts +23 -0
- package/dist/config/yaml.d.ts.map +1 -0
- package/dist/config/yaml.js +108 -0
- package/dist/config/yaml.js.map +1 -0
- package/dist/http/client.d.ts +82 -0
- package/dist/http/client.d.ts.map +1 -0
- package/dist/http/client.js +658 -0
- package/dist/http/client.js.map +1 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/app.d.ts +244 -0
- package/dist/kernel/app.d.ts.map +1 -0
- package/dist/kernel/app.js +654 -0
- package/dist/kernel/app.js.map +1 -0
- package/dist/kernel/kv-sink.d.ts +34 -0
- package/dist/kernel/kv-sink.d.ts.map +1 -0
- package/dist/kernel/kv-sink.js +25 -0
- package/dist/kernel/kv-sink.js.map +1 -0
- package/dist/kernel/policy.d.ts +77 -0
- package/dist/kernel/policy.d.ts.map +1 -0
- package/dist/kernel/policy.js +99 -0
- package/dist/kernel/policy.js.map +1 -0
- package/dist/kernel/runtime.d.ts +117 -0
- package/dist/kernel/runtime.d.ts.map +1 -0
- package/dist/kernel/runtime.js +197 -0
- package/dist/kernel/runtime.js.map +1 -0
- package/dist/kernel/sql-sink.d.ts +36 -0
- package/dist/kernel/sql-sink.d.ts.map +1 -0
- package/dist/kernel/sql-sink.js +114 -0
- package/dist/kernel/sql-sink.js.map +1 -0
- package/dist/logger/format.d.ts +67 -0
- package/dist/logger/format.d.ts.map +1 -0
- package/dist/logger/format.js +218 -0
- package/dist/logger/format.js.map +1 -0
- package/dist/logger/index.d.ts +97 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +363 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/rotate.d.ts +47 -0
- package/dist/logger/rotate.d.ts.map +1 -0
- package/dist/logger/rotate.js +242 -0
- package/dist/logger/rotate.js.map +1 -0
- package/dist/message/segment.d.ts +255 -0
- package/dist/message/segment.d.ts.map +1 -0
- package/dist/message/segment.js +510 -0
- package/dist/message/segment.js.map +1 -0
- package/dist/message/split.d.ts +34 -0
- package/dist/message/split.d.ts.map +1 -0
- package/dist/message/split.js +79 -0
- package/dist/message/split.js.map +1 -0
- package/dist/message/target.d.ts +29 -0
- package/dist/message/target.d.ts.map +1 -0
- package/dist/message/target.js +34 -0
- package/dist/message/target.js.map +1 -0
- package/dist/pipeline/cooldown.d.ts +84 -0
- package/dist/pipeline/cooldown.d.ts.map +1 -0
- package/dist/pipeline/cooldown.js +94 -0
- package/dist/pipeline/cooldown.js.map +1 -0
- package/dist/pipeline/dispatch.d.ts +98 -0
- package/dist/pipeline/dispatch.d.ts.map +1 -0
- package/dist/pipeline/dispatch.js +323 -0
- package/dist/pipeline/dispatch.js.map +1 -0
- package/dist/pipeline/event.d.ts +130 -0
- package/dist/pipeline/event.d.ts.map +1 -0
- package/dist/pipeline/event.js +538 -0
- package/dist/pipeline/event.js.map +1 -0
- package/dist/pipeline/middleware.d.ts +63 -0
- package/dist/pipeline/middleware.d.ts.map +1 -0
- package/dist/pipeline/middleware.js +174 -0
- package/dist/pipeline/middleware.js.map +1 -0
- package/dist/pipeline/prompt.d.ts +80 -0
- package/dist/pipeline/prompt.d.ts.map +1 -0
- package/dist/pipeline/prompt.js +155 -0
- package/dist/pipeline/prompt.js.map +1 -0
- package/dist/pipeline/router.d.ts +98 -0
- package/dist/pipeline/router.d.ts.map +1 -0
- package/dist/pipeline/router.js +489 -0
- package/dist/pipeline/router.js.map +1 -0
- package/dist/platform/detect.d.ts +24 -0
- package/dist/platform/detect.d.ts.map +1 -0
- package/dist/platform/detect.js +178 -0
- package/dist/platform/detect.js.map +1 -0
- package/dist/platform/paths.d.ts +38 -0
- package/dist/platform/paths.d.ts.map +1 -0
- package/dist/platform/paths.js +129 -0
- package/dist/platform/paths.js.map +1 -0
- package/dist/platform/system.d.ts +71 -0
- package/dist/platform/system.d.ts.map +1 -0
- package/dist/platform/system.js +242 -0
- package/dist/platform/system.js.map +1 -0
- package/dist/plugin/context.d.ts +92 -0
- package/dist/plugin/context.d.ts.map +1 -0
- package/dist/plugin/context.js +573 -0
- package/dist/plugin/context.js.map +1 -0
- package/dist/plugin/define.d.ts +92 -0
- package/dist/plugin/define.d.ts.map +1 -0
- package/dist/plugin/define.js +82 -0
- package/dist/plugin/define.js.map +1 -0
- package/dist/plugin/discover.d.ts +106 -0
- package/dist/plugin/discover.d.ts.map +1 -0
- package/dist/plugin/discover.js +254 -0
- package/dist/plugin/discover.js.map +1 -0
- package/dist/plugin/events.d.ts +109 -0
- package/dist/plugin/events.d.ts.map +1 -0
- package/dist/plugin/events.js +209 -0
- package/dist/plugin/events.js.map +1 -0
- package/dist/plugin/hooks.d.ts +266 -0
- package/dist/plugin/hooks.d.ts.map +1 -0
- package/dist/plugin/hooks.js +84 -0
- package/dist/plugin/hooks.js.map +1 -0
- package/dist/plugin/host.d.ts +160 -0
- package/dist/plugin/host.d.ts.map +1 -0
- package/dist/plugin/host.js +546 -0
- package/dist/plugin/host.js.map +1 -0
- package/dist/plugin/market.d.ts +264 -0
- package/dist/plugin/market.d.ts.map +1 -0
- package/dist/plugin/market.js +700 -0
- package/dist/plugin/market.js.map +1 -0
- package/dist/plugin/services.d.ts +116 -0
- package/dist/plugin/services.d.ts.map +1 -0
- package/dist/plugin/services.js +213 -0
- package/dist/plugin/services.js.map +1 -0
- package/dist/plugin/tar.d.ts +65 -0
- package/dist/plugin/tar.d.ts.map +1 -0
- package/dist/plugin/tar.js +277 -0
- package/dist/plugin/tar.js.map +1 -0
- package/dist/render/registry.d.ts +127 -0
- package/dist/render/registry.d.ts.map +1 -0
- package/dist/render/registry.js +306 -0
- package/dist/render/registry.js.map +1 -0
- package/dist/scheduler/index.d.ts +67 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +279 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/server/api.d.ts +179 -0
- package/dist/server/api.d.ts.map +1 -0
- package/dist/server/api.js +602 -0
- package/dist/server/api.js.map +1 -0
- package/dist/server/auth.d.ts +83 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +178 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/browse.d.ts +60 -0
- package/dist/server/browse.d.ts.map +1 -0
- package/dist/server/browse.js +187 -0
- package/dist/server/browse.js.map +1 -0
- package/dist/server/files.d.ts +27 -0
- package/dist/server/files.d.ts.map +1 -0
- package/dist/server/files.js +76 -0
- package/dist/server/files.js.map +1 -0
- package/dist/server/index.d.ts +189 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1123 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/table.d.ts +105 -0
- package/dist/server/table.d.ts.map +1 -0
- package/dist/server/table.js +289 -0
- package/dist/server/table.js.map +1 -0
- package/dist/store/index.d.ts +50 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +120 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/json.d.ts +73 -0
- package/dist/store/json.d.ts.map +1 -0
- package/dist/store/json.js +174 -0
- package/dist/store/json.js.map +1 -0
- package/dist/store/kv.d.ts +126 -0
- package/dist/store/kv.d.ts.map +1 -0
- package/dist/store/kv.js +232 -0
- package/dist/store/kv.js.map +1 -0
- package/dist/store/level.d.ts +103 -0
- package/dist/store/level.d.ts.map +1 -0
- package/dist/store/level.js +119 -0
- package/dist/store/level.js.map +1 -0
- package/dist/store/memory.d.ts +61 -0
- package/dist/store/memory.d.ts.map +1 -0
- package/dist/store/memory.js +94 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/store/sql.d.ts +142 -0
- package/dist/store/sql.d.ts.map +1 -0
- package/dist/store/sql.js +246 -0
- package/dist/store/sql.js.map +1 -0
- package/dist/testing/fake.d.ts +109 -0
- package/dist/testing/fake.d.ts.map +1 -0
- package/dist/testing/fake.js +196 -0
- package/dist/testing/fake.js.map +1 -0
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +16 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/mock-adapter.d.ts +205 -0
- package/dist/testing/mock-adapter.d.ts.map +1 -0
- package/dist/testing/mock-adapter.js +309 -0
- package/dist/testing/mock-adapter.js.map +1 -0
- package/dist/util/deep.d.ts +108 -0
- package/dist/util/deep.d.ts.map +1 -0
- package/dist/util/deep.js +275 -0
- package/dist/util/deep.js.map +1 -0
- package/dist/util/defer.d.ts +107 -0
- package/dist/util/defer.d.ts.map +1 -0
- package/dist/util/defer.js +163 -0
- package/dist/util/defer.js.map +1 -0
- package/dist/util/dispose.d.ts +88 -0
- package/dist/util/dispose.d.ts.map +1 -0
- package/dist/util/dispose.js +143 -0
- package/dist/util/dispose.js.map +1 -0
- package/dist/util/duration.d.ts +28 -0
- package/dist/util/duration.d.ts.map +1 -0
- package/dist/util/duration.js +77 -0
- package/dist/util/duration.js.map +1 -0
- package/dist/util/fs.d.ts +110 -0
- package/dist/util/fs.d.ts.map +1 -0
- package/dist/util/fs.js +269 -0
- package/dist/util/fs.js.map +1 -0
- package/dist/util/id.d.ts +49 -0
- package/dist/util/id.d.ts.map +1 -0
- package/dist/util/id.js +87 -0
- package/dist/util/id.js.map +1 -0
- package/dist/util/lru.d.ts +74 -0
- package/dist/util/lru.d.ts.map +1 -0
- package/dist/util/lru.js +156 -0
- package/dist/util/lru.js.map +1 -0
- package/dist/util/queue.d.ts +64 -0
- package/dist/util/queue.d.ts.map +1 -0
- package/dist/util/queue.js +147 -0
- package/dist/util/queue.js.map +1 -0
- package/dist/util/text.d.ts +86 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +182 -0
- package/dist/util/text.js.map +1 -0
- package/package.json +44 -0
- package/src/adapter/accounts.ts +758 -0
- package/src/adapter/bots.ts +582 -0
- package/src/adapter/host.ts +224 -0
- package/src/adapter/login.ts +481 -0
- package/src/adapter/registry.ts +202 -0
- package/src/config/core-config.test.ts +60 -0
- package/src/config/core-config.ts +401 -0
- package/src/config/schema.test.ts +179 -0
- package/src/config/schema.ts +1036 -0
- package/src/config/store.test.ts +245 -0
- package/src/config/store.ts +506 -0
- package/src/config/yaml.ts +119 -0
- package/src/http/client.test.ts +568 -0
- package/src/http/client.ts +777 -0
- package/src/index.ts +106 -0
- package/src/kernel/app.test.ts +267 -0
- package/src/kernel/app.ts +843 -0
- package/src/kernel/kv-sink.ts +58 -0
- package/src/kernel/policy.ts +120 -0
- package/src/kernel/runtime.test.ts +542 -0
- package/src/kernel/runtime.ts +307 -0
- package/src/kernel/sql-sink.ts +154 -0
- package/src/logger/format.ts +259 -0
- package/src/logger/index.ts +427 -0
- package/src/logger/rotate.ts +265 -0
- package/src/message/segment.test.ts +335 -0
- package/src/message/segment.ts +569 -0
- package/src/message/split.ts +97 -0
- package/src/message/target.ts +48 -0
- package/src/pipeline/cooldown.ts +136 -0
- package/src/pipeline/dispatch.ts +419 -0
- package/src/pipeline/event.ts +735 -0
- package/src/pipeline/middleware.test.ts +426 -0
- package/src/pipeline/middleware.ts +199 -0
- package/src/pipeline/prompt.ts +212 -0
- package/src/pipeline/router.test.ts +432 -0
- package/src/pipeline/router.ts +575 -0
- package/src/platform/detect.ts +178 -0
- package/src/platform/paths.ts +147 -0
- package/src/platform/system.test.ts +102 -0
- package/src/platform/system.ts +289 -0
- package/src/plugin/context.test.ts +459 -0
- package/src/plugin/context.ts +757 -0
- package/src/plugin/define.test.ts +120 -0
- package/src/plugin/define.ts +142 -0
- package/src/plugin/discover.test.ts +253 -0
- package/src/plugin/discover.ts +353 -0
- package/src/plugin/events.test.ts +162 -0
- package/src/plugin/events.ts +266 -0
- package/src/plugin/hooks.ts +373 -0
- package/src/plugin/host.test.ts +696 -0
- package/src/plugin/host.ts +738 -0
- package/src/plugin/market.test.ts +781 -0
- package/src/plugin/market.ts +870 -0
- package/src/plugin/services.test.ts +135 -0
- package/src/plugin/services.ts +261 -0
- package/src/plugin/tar.test.ts +158 -0
- package/src/plugin/tar.ts +309 -0
- package/src/render/registry.test.ts +127 -0
- package/src/render/registry.ts +439 -0
- package/src/scheduler/index.ts +357 -0
- package/src/server/api.test.ts +802 -0
- package/src/server/api.ts +869 -0
- package/src/server/auth.ts +201 -0
- package/src/server/browse.test.ts +193 -0
- package/src/server/browse.ts +240 -0
- package/src/server/files.ts +97 -0
- package/src/server/index.test.ts +566 -0
- package/src/server/index.ts +1386 -0
- package/src/server/table.ts +351 -0
- package/src/store/index.ts +156 -0
- package/src/store/json.ts +198 -0
- package/src/store/kv.test.ts +307 -0
- package/src/store/kv.ts +256 -0
- package/src/store/level.ts +167 -0
- package/src/store/memory.ts +109 -0
- package/src/store/sql.test.ts +204 -0
- package/src/store/sql.ts +324 -0
- package/src/testing/fake.ts +297 -0
- package/src/testing/index.ts +15 -0
- package/src/testing/mock-adapter.ts +588 -0
- package/src/util/deep.ts +272 -0
- package/src/util/defer.ts +211 -0
- package/src/util/dispose.ts +169 -0
- package/src/util/duration.ts +82 -0
- package/src/util/fs.ts +279 -0
- package/src/util/id.ts +94 -0
- package/src/util/lru.ts +185 -0
- package/src/util/queue.ts +167 -0
- package/src/util/text.ts +189 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:tar.gz 解包 —— 把内存中的归档解到目标目录,并在解包阶段执行路径与体积校验
|
|
3
|
+
* 依赖方向:仅依赖 node 标准库与 `util/fs` 的越界校验;不认识插件市场,也不发起网络请求
|
|
4
|
+
* 生命周期:纯函数,无状态
|
|
5
|
+
* 注意事项:该实现不引入第三方解包库。插件市场需要在 Windows 与 Termux 上以相同方式
|
|
6
|
+
* 解包 GitHub 归档,而现有的 tar 实现均带原生模块或较大的依赖树;
|
|
7
|
+
* 归档格式本身只有 512 字节定长头部一种结构,自行解析的成本低于引入依赖。
|
|
8
|
+
*
|
|
9
|
+
* 解包被视为处理不可信输入。三项校验在写盘之前完成:
|
|
10
|
+
* 1) 每个条目的目标路径经 `safeJoin` 求值,越出目标目录即丢弃该条目;
|
|
11
|
+
* 2) 符号链接与硬链接条目一律丢弃 —— 归档可借链接把后续写入导向目录之外,
|
|
12
|
+
* 而插件分发不需要链接;
|
|
13
|
+
* 3) 解包后总字节数与条目数均设上限,避免高压缩比归档耗尽磁盘。
|
|
14
|
+
*/
|
|
15
|
+
import { gunzipSync, gzipSync } from "node:zlib"
|
|
16
|
+
import { mkdir, writeFile } from "node:fs/promises"
|
|
17
|
+
import { dirname } from "node:path"
|
|
18
|
+
import { safeJoin } from "../util/fs.js"
|
|
19
|
+
|
|
20
|
+
/** tar 块大小 */
|
|
21
|
+
const BLOCK = 512
|
|
22
|
+
|
|
23
|
+
/** 头部各字段的偏移与长度 */
|
|
24
|
+
const FIELD = {
|
|
25
|
+
name: [0, 100],
|
|
26
|
+
size: [124, 12],
|
|
27
|
+
typeflag: [156, 1],
|
|
28
|
+
prefix: [345, 155]
|
|
29
|
+
} as const
|
|
30
|
+
|
|
31
|
+
/** 解包后总字节数的缺省上限 */
|
|
32
|
+
const DEFAULT_MAX_BYTES = 256 * 1024 * 1024
|
|
33
|
+
|
|
34
|
+
/** 条目数的缺省上限 */
|
|
35
|
+
const DEFAULT_MAX_ENTRIES = 20_000
|
|
36
|
+
|
|
37
|
+
/** 解包选项 */
|
|
38
|
+
export interface ExtractOptions {
|
|
39
|
+
/** 剥离前若干层路径,GitHub 归档带一层仓库名目录,取 1 */
|
|
40
|
+
readonly strip?: number
|
|
41
|
+
/** 解包后总字节上限,超出即抛错 */
|
|
42
|
+
readonly maxBytes?: number
|
|
43
|
+
/** 条目数上限,超出即抛错 */
|
|
44
|
+
readonly maxEntries?: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 一个已解析的条目头部 */
|
|
48
|
+
interface TarHeader {
|
|
49
|
+
/** 归档内路径 */
|
|
50
|
+
readonly path: string
|
|
51
|
+
/** 内容字节数 */
|
|
52
|
+
readonly size: number
|
|
53
|
+
/** 类型标记 */
|
|
54
|
+
readonly type: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 按 NUL 截断并去除首尾空白
|
|
59
|
+
* @param buf 归档缓冲
|
|
60
|
+
* @param offset 起始偏移
|
|
61
|
+
* @param length 字段长度
|
|
62
|
+
* @returns 字段文本
|
|
63
|
+
*/
|
|
64
|
+
function textAt(buf: Uint8Array, offset: number, length: number): string {
|
|
65
|
+
const slice = buf.subarray(offset, offset + length)
|
|
66
|
+
const end = slice.indexOf(0)
|
|
67
|
+
return Buffer.from(end === -1 ? slice : slice.subarray(0, end))
|
|
68
|
+
.toString("utf8")
|
|
69
|
+
.trim()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 读取一个八进制数值字段
|
|
74
|
+
*
|
|
75
|
+
* 体积字段在超过 8GiB 时会改用 base-256 编码(首字节最高位置 1)。插件归档不会
|
|
76
|
+
* 触及该量级,因此这里遇到 base-256 直接判为非法,而不是实现一段无从验证的分支。
|
|
77
|
+
* @param buf 归档缓冲
|
|
78
|
+
* @param offset 起始偏移
|
|
79
|
+
* @param length 字段长度
|
|
80
|
+
* @returns 数值
|
|
81
|
+
* @throws 字段为 base-256 编码或不是合法八进制时
|
|
82
|
+
*/
|
|
83
|
+
function octalAt(buf: Uint8Array, offset: number, length: number): number {
|
|
84
|
+
const first = buf[offset] ?? 0
|
|
85
|
+
if ((first & 0x80) !== 0) throw new Error("归档使用了 base-256 数值字段,超出支持范围")
|
|
86
|
+
const text = textAt(buf, offset, length)
|
|
87
|
+
if (text === "") return 0
|
|
88
|
+
const value = Number.parseInt(text, 8)
|
|
89
|
+
if (!Number.isFinite(value) || value < 0) throw new Error(`归档头部的数值字段非法:${text}`)
|
|
90
|
+
return value
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 判断一个块是否全为零字节
|
|
95
|
+
* @param buf 归档缓冲
|
|
96
|
+
* @param offset 块起始偏移
|
|
97
|
+
* @returns 是否为结束块
|
|
98
|
+
*/
|
|
99
|
+
function isZeroBlock(buf: Uint8Array, offset: number): boolean {
|
|
100
|
+
for (let i = offset; i < offset + BLOCK; i++) if (buf[i] !== 0) return false
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 解析一个头部块
|
|
106
|
+
* @param buf 归档缓冲
|
|
107
|
+
* @param offset 块起始偏移
|
|
108
|
+
* @returns 头部信息
|
|
109
|
+
*/
|
|
110
|
+
function readHeader(buf: Uint8Array, offset: number): TarHeader {
|
|
111
|
+
const name = textAt(buf, offset + FIELD.name[0], FIELD.name[1])
|
|
112
|
+
const prefix = textAt(buf, offset + FIELD.prefix[0], FIELD.prefix[1])
|
|
113
|
+
return {
|
|
114
|
+
path: prefix === "" ? name : `${prefix}/${name}`,
|
|
115
|
+
size: octalAt(buf, offset + FIELD.size[0], FIELD.size[1]),
|
|
116
|
+
type: textAt(buf, offset + FIELD.typeflag[0], FIELD.typeflag[1]) || "0"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 从 pax 扩展头部中取出 `path` 记录
|
|
122
|
+
*
|
|
123
|
+
* 记录格式为 `长度 键=值\n`,长度含自身。仅取 `path`,其余记录(权限、时间)
|
|
124
|
+
* 与插件分发无关。
|
|
125
|
+
* @param block 扩展头部的内容
|
|
126
|
+
* @returns 路径;未声明时 undefined
|
|
127
|
+
*/
|
|
128
|
+
function paxPath(block: Uint8Array): string | undefined {
|
|
129
|
+
const text = Buffer.from(block).toString("utf8")
|
|
130
|
+
const match = /\d+ path=([^\n]*)\n/.exec(text)
|
|
131
|
+
return match?.[1]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 剥离路径的前若干层
|
|
136
|
+
* @param path 归档内路径
|
|
137
|
+
* @param strip 剥离层数
|
|
138
|
+
* @returns 剥离后的相对路径;剥空时为空串
|
|
139
|
+
*/
|
|
140
|
+
function stripPath(path: string, strip: number): string {
|
|
141
|
+
if (strip <= 0) return path
|
|
142
|
+
const parts = path.split("/").filter(part => part !== "")
|
|
143
|
+
return parts.slice(strip).join("/")
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 把归档条目写入目标目录
|
|
148
|
+
*
|
|
149
|
+
* 目标目录会被按需创建。返回值为实际写入的相对路径列表,供调用方在日志中说明
|
|
150
|
+
* 安装内容,也便于单元测试断言。
|
|
151
|
+
* @param archive gzip 压缩后的 tar 归档
|
|
152
|
+
* @param dest 目标目录绝对路径
|
|
153
|
+
* @param opts 解包选项
|
|
154
|
+
* @returns 实际写入的相对路径列表
|
|
155
|
+
* @throws 归档结构非法、超出体积或条目上限时
|
|
156
|
+
*/
|
|
157
|
+
export async function extractTarGz(archive: Uint8Array, dest: string, opts: ExtractOptions = {}): Promise<string[]> {
|
|
158
|
+
const buf = gunzipSync(archive)
|
|
159
|
+
const strip = opts.strip ?? 0
|
|
160
|
+
const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES
|
|
161
|
+
const maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES
|
|
162
|
+
|
|
163
|
+
const written: string[] = []
|
|
164
|
+
let total = 0
|
|
165
|
+
let entries = 0
|
|
166
|
+
/** 由 pax 或 GNU 长名头部预告的下一条目路径 */
|
|
167
|
+
let pendingPath: string | undefined
|
|
168
|
+
let offset = 0
|
|
169
|
+
|
|
170
|
+
while (offset + BLOCK <= buf.length) {
|
|
171
|
+
if (isZeroBlock(buf, offset)) break
|
|
172
|
+
|
|
173
|
+
const header = readHeader(buf, offset)
|
|
174
|
+
offset += BLOCK
|
|
175
|
+
const contentBlocks = Math.ceil(header.size / BLOCK)
|
|
176
|
+
const content = buf.subarray(offset, offset + header.size)
|
|
177
|
+
offset += contentBlocks * BLOCK
|
|
178
|
+
|
|
179
|
+
// 扩展头部本身不落盘,只影响下一个条目的路径
|
|
180
|
+
if (header.type === "x" || header.type === "X") {
|
|
181
|
+
pendingPath = paxPath(content)
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
if (header.type === "L") {
|
|
185
|
+
pendingPath = Buffer.from(content).toString("utf8").replace(/\0+$/, "")
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
// 全局头部与链接条目一律丢弃,理由见文件头
|
|
189
|
+
if (header.type === "g" || header.type === "1" || header.type === "2") {
|
|
190
|
+
pendingPath = undefined
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const rawPath = pendingPath ?? header.path
|
|
195
|
+
pendingPath = undefined
|
|
196
|
+
const rel = stripPath(rawPath, strip)
|
|
197
|
+
if (rel === "") continue
|
|
198
|
+
|
|
199
|
+
if (++entries > maxEntries) throw new Error(`归档条目数超过上限 ${maxEntries}`)
|
|
200
|
+
total += header.size
|
|
201
|
+
if (total > maxBytes) throw new Error(`归档解包后体积超过上限 ${maxBytes} 字节`)
|
|
202
|
+
|
|
203
|
+
let target: string
|
|
204
|
+
try {
|
|
205
|
+
target = safeJoin(dest, rel)
|
|
206
|
+
} catch {
|
|
207
|
+
// 越界条目直接跳过:归档由第三方提供,不应因其内容异常而中断整次安装,
|
|
208
|
+
// 但也绝不能按其要求写到目标目录之外
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (header.type === "5") {
|
|
213
|
+
await mkdir(target, { recursive: true })
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
216
|
+
if (header.type !== "0") continue
|
|
217
|
+
|
|
218
|
+
await mkdir(dirname(target), { recursive: true })
|
|
219
|
+
await writeFile(target, content)
|
|
220
|
+
written.push(rel)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return written
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 判断目录内是否只有一个顶层目录
|
|
228
|
+
*
|
|
229
|
+
* GitHub 的归档统一带一层 `仓库名-提交号/` 包装,而用户自建的归档可能没有。
|
|
230
|
+
* 调用方据此决定 `strip` 取 0 还是 1。
|
|
231
|
+
* @param paths 归档内的相对路径列表
|
|
232
|
+
* @returns 唯一的顶层目录名;不唯一或存在顶层文件时 undefined
|
|
233
|
+
*/
|
|
234
|
+
export function singleRoot(paths: readonly string[]): string | undefined {
|
|
235
|
+
const roots = new Set<string>()
|
|
236
|
+
for (const path of paths) {
|
|
237
|
+
const first = path.split("/")[0] ?? ""
|
|
238
|
+
if (first === path) return undefined
|
|
239
|
+
roots.add(first)
|
|
240
|
+
}
|
|
241
|
+
return roots.size === 1 ? [...roots][0] : undefined
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 拼一个位于目标目录内的路径,越界时抛错
|
|
246
|
+
*
|
|
247
|
+
* 供插件市场在删除前复核目标路径,语义与解包阶段一致。
|
|
248
|
+
* @param base 基准目录
|
|
249
|
+
* @param name 名称
|
|
250
|
+
* @returns 绝对路径
|
|
251
|
+
* @throws 越界时
|
|
252
|
+
*/
|
|
253
|
+
export function joinWithin(base: string, name: string): string {
|
|
254
|
+
return safeJoin(base, name)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** 供测试与调用方复用的块大小 */
|
|
258
|
+
export const TAR_BLOCK_SIZE = BLOCK
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 构造一个 tar 头部块
|
|
262
|
+
*
|
|
263
|
+
* 该函数只在测试与自建归档时使用;生产路径只解包不打包。之所以放在本文件而不是
|
|
264
|
+
* 测试文件里,是为了让头部字段的偏移只有一份定义 —— 打包与解包共用 `FIELD`,
|
|
265
|
+
* 偏移写错时两侧同时失败,不会出现"测试自己造的归档能解、真实归档不能解"。
|
|
266
|
+
* @param path 条目路径
|
|
267
|
+
* @param size 内容字节数
|
|
268
|
+
* @param type 类型标记
|
|
269
|
+
* @returns 512 字节头部
|
|
270
|
+
*/
|
|
271
|
+
export function buildTarHeader(path: string, size: number, type: string): Buffer {
|
|
272
|
+
const block = Buffer.alloc(BLOCK)
|
|
273
|
+
block.write(path.slice(0, FIELD.name[1]), FIELD.name[0], "utf8")
|
|
274
|
+
block.write(`${size.toString(8).padStart(11, "0")}\0`, FIELD.size[0], "ascii")
|
|
275
|
+
block.write("0000644\0", 100, "ascii")
|
|
276
|
+
block.write("0000000\0", 108, "ascii")
|
|
277
|
+
block.write("0000000\0", 116, "ascii")
|
|
278
|
+
block.write(`${Math.floor(Date.now() / 1000).toString(8).padStart(11, "0")}\0`, 136, "ascii")
|
|
279
|
+
block.write(type, FIELD.typeflag[0], "ascii")
|
|
280
|
+
block.write("ustar\0" + "00", 257, "ascii")
|
|
281
|
+
// 校验和字段先填空格再求和,这是格式规定的计算方式
|
|
282
|
+
block.write(" ", 148, "ascii")
|
|
283
|
+
let sum = 0
|
|
284
|
+
for (const byte of block) sum += byte
|
|
285
|
+
block.write(`${sum.toString(8).padStart(6, "0")}\0 `, 148, "ascii")
|
|
286
|
+
return block
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* 把若干文件打成 gzip 压缩的 tar 归档
|
|
291
|
+
* @param files 条目列表,路径使用正斜杠
|
|
292
|
+
* @returns 压缩后的归档
|
|
293
|
+
*/
|
|
294
|
+
export function buildTarGz(files: readonly { path: string; content: string | Uint8Array; type?: string }[]): Buffer {
|
|
295
|
+
const parts: Buffer[] = []
|
|
296
|
+
for (const file of files) {
|
|
297
|
+
const content = typeof file.content === "string" ? Buffer.from(file.content, "utf8") : Buffer.from(file.content)
|
|
298
|
+
const type = file.type ?? "0"
|
|
299
|
+
parts.push(buildTarHeader(file.path, type === "5" ? 0 : content.length, type))
|
|
300
|
+
if (type !== "5" && content.length > 0) {
|
|
301
|
+
const padded = Buffer.alloc(Math.ceil(content.length / BLOCK) * BLOCK)
|
|
302
|
+
content.copy(padded)
|
|
303
|
+
parts.push(padded)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
parts.push(Buffer.alloc(BLOCK * 2))
|
|
307
|
+
return gzipSync(Buffer.concat(parts))
|
|
308
|
+
}
|
|
309
|
+
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:渲染注册表的埋点用例 —— `render/done` 与「渲染完成」日志行
|
|
3
|
+
* 依赖方向:测试文件,依赖 render/registry 与 testing/fake
|
|
4
|
+
* 生命周期:每个用例新建一个注册表
|
|
5
|
+
* 注意事项:只测**埋点那一部分**(日志级别、字节数、成败两路都发事件),
|
|
6
|
+
* 注册表原有的择一、重试、可用性缓存等行为不在此处重复覆盖。
|
|
7
|
+
*
|
|
8
|
+
* 用真实的 `RenderRegistry` 配一个假渲染器,而不是 mock 注册表本身:
|
|
9
|
+
* 「全部渲染器失败时也要发一次事件」这条只在真实的重试与切换流程走完
|
|
10
|
+
* 之后才成立,替身对象测不到。
|
|
11
|
+
*/
|
|
12
|
+
import { describe, expect, it } from "vitest"
|
|
13
|
+
import type { RenderDoneInfo, RenderRequest, RenderResult, RendererProvider } from "@yunzai-ng/types"
|
|
14
|
+
import { fakeLogger, type FakeLogger } from "../testing/fake.js"
|
|
15
|
+
import { RenderRegistry, type RenderPolicy } from "./registry.js"
|
|
16
|
+
|
|
17
|
+
/** 不重试、不超时的策略,使用例只观察埋点 */
|
|
18
|
+
const POLICY: RenderPolicy = { default: "fake", timeout: 5000, retry: 0, quality: 90, scale: 1 }
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 造一个最小渲染请求
|
|
22
|
+
* @param template 模板标识
|
|
23
|
+
* @returns 渲染请求
|
|
24
|
+
*/
|
|
25
|
+
function request(template = "demo/card"): RenderRequest {
|
|
26
|
+
return { template, data: {}, templateRoot: "/tpl", resourceRoot: "/res", origin: "demo-plugin" }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 造一个假渲染器
|
|
31
|
+
* @param impl 出图实现;抛错即视作渲染失败
|
|
32
|
+
* @returns 渲染器实现
|
|
33
|
+
*/
|
|
34
|
+
function fakeRenderer(impl: () => Promise<RenderResult>): RendererProvider {
|
|
35
|
+
return {
|
|
36
|
+
id: "fake",
|
|
37
|
+
name: "假渲染器",
|
|
38
|
+
available: () => Promise.resolve(true),
|
|
39
|
+
render: () => impl()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 一套注册表与它收到的埋点 */
|
|
44
|
+
interface Fixture {
|
|
45
|
+
/** 注册表 */
|
|
46
|
+
registry: RenderRegistry
|
|
47
|
+
/** 假日志器,断言日志行用 */
|
|
48
|
+
logger: FakeLogger
|
|
49
|
+
/** 收到的 `render/done` 载荷,按顺序 */
|
|
50
|
+
done: RenderDoneInfo[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 建一套夹具
|
|
55
|
+
* @param impl 假渲染器的出图实现
|
|
56
|
+
* @returns 夹具
|
|
57
|
+
*/
|
|
58
|
+
function setup(impl: () => Promise<RenderResult>): Fixture {
|
|
59
|
+
const logger = fakeLogger()
|
|
60
|
+
const done: RenderDoneInfo[] = []
|
|
61
|
+
const registry = new RenderRegistry({ logger, policy: () => POLICY, onDone: info => done.push(info) })
|
|
62
|
+
registry.register(fakeRenderer(impl), "fake-plugin")
|
|
63
|
+
return { registry, logger, done }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("渲染埋点", () => {
|
|
67
|
+
it("出图成功:发一次 render/done,带张数、字节数与耗时", async () => {
|
|
68
|
+
const { registry, done } = setup(() =>
|
|
69
|
+
Promise.resolve({ images: [new Uint8Array(1024), new Uint8Array(2048)], cost: 123, renderer: "fake" })
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
await registry.render(request())
|
|
73
|
+
|
|
74
|
+
expect(done).toHaveLength(1)
|
|
75
|
+
expect(done[0]).toMatchObject({
|
|
76
|
+
renderer: "fake",
|
|
77
|
+
template: "demo/card",
|
|
78
|
+
images: 2,
|
|
79
|
+
bytes: 3072,
|
|
80
|
+
cost: 123,
|
|
81
|
+
ok: true
|
|
82
|
+
})
|
|
83
|
+
expect(done[0]?.error).toBeUndefined()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("「渲染完成」记在 info 而非 debug —— 默认级别下要看得见", async () => {
|
|
87
|
+
const { registry, logger } = setup(() =>
|
|
88
|
+
Promise.resolve({ images: [new Uint8Array(2048)], cost: 88, renderer: "fake" })
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
await registry.render(request())
|
|
92
|
+
|
|
93
|
+
const line = logger.lines.find(l => l.includes("渲染完成"))
|
|
94
|
+
expect(line).toBeDefined()
|
|
95
|
+
expect(line?.startsWith("info ")).toBe(true)
|
|
96
|
+
// 字节数以可读形式附在行内:出图异常大往往是模板漏了尺寸约束,只看张数看不出来
|
|
97
|
+
expect(line).toContain("2.00KB")
|
|
98
|
+
expect(line).toContain("88ms")
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it("全部渲染器失败:照旧抛出,但也发一次 ok=false 的 render/done", async () => {
|
|
102
|
+
const { registry, done } = setup(() => Promise.reject(new Error("Chromium 没装")))
|
|
103
|
+
|
|
104
|
+
await expect(registry.render(request("demo/fail"))).rejects.toThrow("渲染失败")
|
|
105
|
+
|
|
106
|
+
// 只在成功时发事件的话,统计出的成功率恒为 100%
|
|
107
|
+
expect(done).toHaveLength(1)
|
|
108
|
+
expect(done[0]?.ok).toBe(false)
|
|
109
|
+
expect(done[0]?.template).toBe("demo/fail")
|
|
110
|
+
expect(done[0]?.images).toBe(0)
|
|
111
|
+
expect(done[0]?.error).toContain("Chromium 没装")
|
|
112
|
+
// 这一次没有任何渲染器出图,renderer 填谁都是错的
|
|
113
|
+
expect(done[0]?.renderer).toBe("")
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it("不传 onDone 也能正常渲染(现有调用方无须改动)", async () => {
|
|
117
|
+
const logger = fakeLogger()
|
|
118
|
+
const registry = new RenderRegistry({ logger, policy: () => POLICY })
|
|
119
|
+
registry.register(
|
|
120
|
+
fakeRenderer(() => Promise.resolve({ images: [new Uint8Array(16)], cost: 5, renderer: "fake" })),
|
|
121
|
+
"fake-plugin"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
const result = await registry.render(request())
|
|
125
|
+
expect(result.images).toHaveLength(1)
|
|
126
|
+
})
|
|
127
|
+
})
|