@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.
Files changed (357) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +51 -0
  3. package/dist/adapter/accounts.d.ts +174 -0
  4. package/dist/adapter/accounts.d.ts.map +1 -0
  5. package/dist/adapter/accounts.js +653 -0
  6. package/dist/adapter/accounts.js.map +1 -0
  7. package/dist/adapter/bots.d.ts +142 -0
  8. package/dist/adapter/bots.d.ts.map +1 -0
  9. package/dist/adapter/bots.js +454 -0
  10. package/dist/adapter/bots.js.map +1 -0
  11. package/dist/adapter/host.d.ts +114 -0
  12. package/dist/adapter/host.d.ts.map +1 -0
  13. package/dist/adapter/host.js +90 -0
  14. package/dist/adapter/host.js.map +1 -0
  15. package/dist/adapter/login.d.ts +174 -0
  16. package/dist/adapter/login.d.ts.map +1 -0
  17. package/dist/adapter/login.js +345 -0
  18. package/dist/adapter/login.js.map +1 -0
  19. package/dist/adapter/registry.d.ts +102 -0
  20. package/dist/adapter/registry.d.ts.map +1 -0
  21. package/dist/adapter/registry.js +150 -0
  22. package/dist/adapter/registry.js.map +1 -0
  23. package/dist/config/core-config.d.ts +151 -0
  24. package/dist/config/core-config.d.ts.map +1 -0
  25. package/dist/config/core-config.js +338 -0
  26. package/dist/config/core-config.js.map +1 -0
  27. package/dist/config/schema.d.ts +449 -0
  28. package/dist/config/schema.d.ts.map +1 -0
  29. package/dist/config/schema.js +871 -0
  30. package/dist/config/schema.js.map +1 -0
  31. package/dist/config/store.d.ts +184 -0
  32. package/dist/config/store.d.ts.map +1 -0
  33. package/dist/config/store.js +428 -0
  34. package/dist/config/store.js.map +1 -0
  35. package/dist/config/yaml.d.ts +23 -0
  36. package/dist/config/yaml.d.ts.map +1 -0
  37. package/dist/config/yaml.js +108 -0
  38. package/dist/config/yaml.js.map +1 -0
  39. package/dist/http/client.d.ts +82 -0
  40. package/dist/http/client.d.ts.map +1 -0
  41. package/dist/http/client.js +658 -0
  42. package/dist/http/client.js.map +1 -0
  43. package/dist/index.d.ts +77 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +94 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/kernel/app.d.ts +244 -0
  48. package/dist/kernel/app.d.ts.map +1 -0
  49. package/dist/kernel/app.js +654 -0
  50. package/dist/kernel/app.js.map +1 -0
  51. package/dist/kernel/kv-sink.d.ts +34 -0
  52. package/dist/kernel/kv-sink.d.ts.map +1 -0
  53. package/dist/kernel/kv-sink.js +25 -0
  54. package/dist/kernel/kv-sink.js.map +1 -0
  55. package/dist/kernel/policy.d.ts +77 -0
  56. package/dist/kernel/policy.d.ts.map +1 -0
  57. package/dist/kernel/policy.js +99 -0
  58. package/dist/kernel/policy.js.map +1 -0
  59. package/dist/kernel/runtime.d.ts +117 -0
  60. package/dist/kernel/runtime.d.ts.map +1 -0
  61. package/dist/kernel/runtime.js +197 -0
  62. package/dist/kernel/runtime.js.map +1 -0
  63. package/dist/kernel/sql-sink.d.ts +36 -0
  64. package/dist/kernel/sql-sink.d.ts.map +1 -0
  65. package/dist/kernel/sql-sink.js +114 -0
  66. package/dist/kernel/sql-sink.js.map +1 -0
  67. package/dist/logger/format.d.ts +67 -0
  68. package/dist/logger/format.d.ts.map +1 -0
  69. package/dist/logger/format.js +218 -0
  70. package/dist/logger/format.js.map +1 -0
  71. package/dist/logger/index.d.ts +97 -0
  72. package/dist/logger/index.d.ts.map +1 -0
  73. package/dist/logger/index.js +363 -0
  74. package/dist/logger/index.js.map +1 -0
  75. package/dist/logger/rotate.d.ts +47 -0
  76. package/dist/logger/rotate.d.ts.map +1 -0
  77. package/dist/logger/rotate.js +242 -0
  78. package/dist/logger/rotate.js.map +1 -0
  79. package/dist/message/segment.d.ts +255 -0
  80. package/dist/message/segment.d.ts.map +1 -0
  81. package/dist/message/segment.js +510 -0
  82. package/dist/message/segment.js.map +1 -0
  83. package/dist/message/split.d.ts +34 -0
  84. package/dist/message/split.d.ts.map +1 -0
  85. package/dist/message/split.js +79 -0
  86. package/dist/message/split.js.map +1 -0
  87. package/dist/message/target.d.ts +29 -0
  88. package/dist/message/target.d.ts.map +1 -0
  89. package/dist/message/target.js +34 -0
  90. package/dist/message/target.js.map +1 -0
  91. package/dist/pipeline/cooldown.d.ts +84 -0
  92. package/dist/pipeline/cooldown.d.ts.map +1 -0
  93. package/dist/pipeline/cooldown.js +94 -0
  94. package/dist/pipeline/cooldown.js.map +1 -0
  95. package/dist/pipeline/dispatch.d.ts +98 -0
  96. package/dist/pipeline/dispatch.d.ts.map +1 -0
  97. package/dist/pipeline/dispatch.js +323 -0
  98. package/dist/pipeline/dispatch.js.map +1 -0
  99. package/dist/pipeline/event.d.ts +130 -0
  100. package/dist/pipeline/event.d.ts.map +1 -0
  101. package/dist/pipeline/event.js +538 -0
  102. package/dist/pipeline/event.js.map +1 -0
  103. package/dist/pipeline/middleware.d.ts +63 -0
  104. package/dist/pipeline/middleware.d.ts.map +1 -0
  105. package/dist/pipeline/middleware.js +174 -0
  106. package/dist/pipeline/middleware.js.map +1 -0
  107. package/dist/pipeline/prompt.d.ts +80 -0
  108. package/dist/pipeline/prompt.d.ts.map +1 -0
  109. package/dist/pipeline/prompt.js +155 -0
  110. package/dist/pipeline/prompt.js.map +1 -0
  111. package/dist/pipeline/router.d.ts +98 -0
  112. package/dist/pipeline/router.d.ts.map +1 -0
  113. package/dist/pipeline/router.js +489 -0
  114. package/dist/pipeline/router.js.map +1 -0
  115. package/dist/platform/detect.d.ts +24 -0
  116. package/dist/platform/detect.d.ts.map +1 -0
  117. package/dist/platform/detect.js +178 -0
  118. package/dist/platform/detect.js.map +1 -0
  119. package/dist/platform/paths.d.ts +38 -0
  120. package/dist/platform/paths.d.ts.map +1 -0
  121. package/dist/platform/paths.js +129 -0
  122. package/dist/platform/paths.js.map +1 -0
  123. package/dist/platform/system.d.ts +71 -0
  124. package/dist/platform/system.d.ts.map +1 -0
  125. package/dist/platform/system.js +242 -0
  126. package/dist/platform/system.js.map +1 -0
  127. package/dist/plugin/context.d.ts +92 -0
  128. package/dist/plugin/context.d.ts.map +1 -0
  129. package/dist/plugin/context.js +573 -0
  130. package/dist/plugin/context.js.map +1 -0
  131. package/dist/plugin/define.d.ts +92 -0
  132. package/dist/plugin/define.d.ts.map +1 -0
  133. package/dist/plugin/define.js +82 -0
  134. package/dist/plugin/define.js.map +1 -0
  135. package/dist/plugin/discover.d.ts +106 -0
  136. package/dist/plugin/discover.d.ts.map +1 -0
  137. package/dist/plugin/discover.js +254 -0
  138. package/dist/plugin/discover.js.map +1 -0
  139. package/dist/plugin/events.d.ts +109 -0
  140. package/dist/plugin/events.d.ts.map +1 -0
  141. package/dist/plugin/events.js +209 -0
  142. package/dist/plugin/events.js.map +1 -0
  143. package/dist/plugin/hooks.d.ts +266 -0
  144. package/dist/plugin/hooks.d.ts.map +1 -0
  145. package/dist/plugin/hooks.js +84 -0
  146. package/dist/plugin/hooks.js.map +1 -0
  147. package/dist/plugin/host.d.ts +160 -0
  148. package/dist/plugin/host.d.ts.map +1 -0
  149. package/dist/plugin/host.js +546 -0
  150. package/dist/plugin/host.js.map +1 -0
  151. package/dist/plugin/market.d.ts +264 -0
  152. package/dist/plugin/market.d.ts.map +1 -0
  153. package/dist/plugin/market.js +700 -0
  154. package/dist/plugin/market.js.map +1 -0
  155. package/dist/plugin/services.d.ts +116 -0
  156. package/dist/plugin/services.d.ts.map +1 -0
  157. package/dist/plugin/services.js +213 -0
  158. package/dist/plugin/services.js.map +1 -0
  159. package/dist/plugin/tar.d.ts +65 -0
  160. package/dist/plugin/tar.d.ts.map +1 -0
  161. package/dist/plugin/tar.js +277 -0
  162. package/dist/plugin/tar.js.map +1 -0
  163. package/dist/render/registry.d.ts +127 -0
  164. package/dist/render/registry.d.ts.map +1 -0
  165. package/dist/render/registry.js +306 -0
  166. package/dist/render/registry.js.map +1 -0
  167. package/dist/scheduler/index.d.ts +67 -0
  168. package/dist/scheduler/index.d.ts.map +1 -0
  169. package/dist/scheduler/index.js +279 -0
  170. package/dist/scheduler/index.js.map +1 -0
  171. package/dist/server/api.d.ts +179 -0
  172. package/dist/server/api.d.ts.map +1 -0
  173. package/dist/server/api.js +602 -0
  174. package/dist/server/api.js.map +1 -0
  175. package/dist/server/auth.d.ts +83 -0
  176. package/dist/server/auth.d.ts.map +1 -0
  177. package/dist/server/auth.js +178 -0
  178. package/dist/server/auth.js.map +1 -0
  179. package/dist/server/browse.d.ts +60 -0
  180. package/dist/server/browse.d.ts.map +1 -0
  181. package/dist/server/browse.js +187 -0
  182. package/dist/server/browse.js.map +1 -0
  183. package/dist/server/files.d.ts +27 -0
  184. package/dist/server/files.d.ts.map +1 -0
  185. package/dist/server/files.js +76 -0
  186. package/dist/server/files.js.map +1 -0
  187. package/dist/server/index.d.ts +189 -0
  188. package/dist/server/index.d.ts.map +1 -0
  189. package/dist/server/index.js +1123 -0
  190. package/dist/server/index.js.map +1 -0
  191. package/dist/server/table.d.ts +105 -0
  192. package/dist/server/table.d.ts.map +1 -0
  193. package/dist/server/table.js +289 -0
  194. package/dist/server/table.js.map +1 -0
  195. package/dist/store/index.d.ts +50 -0
  196. package/dist/store/index.d.ts.map +1 -0
  197. package/dist/store/index.js +120 -0
  198. package/dist/store/index.js.map +1 -0
  199. package/dist/store/json.d.ts +73 -0
  200. package/dist/store/json.d.ts.map +1 -0
  201. package/dist/store/json.js +174 -0
  202. package/dist/store/json.js.map +1 -0
  203. package/dist/store/kv.d.ts +126 -0
  204. package/dist/store/kv.d.ts.map +1 -0
  205. package/dist/store/kv.js +232 -0
  206. package/dist/store/kv.js.map +1 -0
  207. package/dist/store/level.d.ts +103 -0
  208. package/dist/store/level.d.ts.map +1 -0
  209. package/dist/store/level.js +119 -0
  210. package/dist/store/level.js.map +1 -0
  211. package/dist/store/memory.d.ts +61 -0
  212. package/dist/store/memory.d.ts.map +1 -0
  213. package/dist/store/memory.js +94 -0
  214. package/dist/store/memory.js.map +1 -0
  215. package/dist/store/sql.d.ts +142 -0
  216. package/dist/store/sql.d.ts.map +1 -0
  217. package/dist/store/sql.js +246 -0
  218. package/dist/store/sql.js.map +1 -0
  219. package/dist/testing/fake.d.ts +109 -0
  220. package/dist/testing/fake.d.ts.map +1 -0
  221. package/dist/testing/fake.js +196 -0
  222. package/dist/testing/fake.js.map +1 -0
  223. package/dist/testing/index.d.ts +16 -0
  224. package/dist/testing/index.d.ts.map +1 -0
  225. package/dist/testing/index.js +16 -0
  226. package/dist/testing/index.js.map +1 -0
  227. package/dist/testing/mock-adapter.d.ts +205 -0
  228. package/dist/testing/mock-adapter.d.ts.map +1 -0
  229. package/dist/testing/mock-adapter.js +309 -0
  230. package/dist/testing/mock-adapter.js.map +1 -0
  231. package/dist/util/deep.d.ts +108 -0
  232. package/dist/util/deep.d.ts.map +1 -0
  233. package/dist/util/deep.js +275 -0
  234. package/dist/util/deep.js.map +1 -0
  235. package/dist/util/defer.d.ts +107 -0
  236. package/dist/util/defer.d.ts.map +1 -0
  237. package/dist/util/defer.js +163 -0
  238. package/dist/util/defer.js.map +1 -0
  239. package/dist/util/dispose.d.ts +88 -0
  240. package/dist/util/dispose.d.ts.map +1 -0
  241. package/dist/util/dispose.js +143 -0
  242. package/dist/util/dispose.js.map +1 -0
  243. package/dist/util/duration.d.ts +28 -0
  244. package/dist/util/duration.d.ts.map +1 -0
  245. package/dist/util/duration.js +77 -0
  246. package/dist/util/duration.js.map +1 -0
  247. package/dist/util/fs.d.ts +110 -0
  248. package/dist/util/fs.d.ts.map +1 -0
  249. package/dist/util/fs.js +269 -0
  250. package/dist/util/fs.js.map +1 -0
  251. package/dist/util/id.d.ts +49 -0
  252. package/dist/util/id.d.ts.map +1 -0
  253. package/dist/util/id.js +87 -0
  254. package/dist/util/id.js.map +1 -0
  255. package/dist/util/lru.d.ts +74 -0
  256. package/dist/util/lru.d.ts.map +1 -0
  257. package/dist/util/lru.js +156 -0
  258. package/dist/util/lru.js.map +1 -0
  259. package/dist/util/queue.d.ts +64 -0
  260. package/dist/util/queue.d.ts.map +1 -0
  261. package/dist/util/queue.js +147 -0
  262. package/dist/util/queue.js.map +1 -0
  263. package/dist/util/text.d.ts +86 -0
  264. package/dist/util/text.d.ts.map +1 -0
  265. package/dist/util/text.js +182 -0
  266. package/dist/util/text.js.map +1 -0
  267. package/package.json +44 -0
  268. package/src/adapter/accounts.ts +758 -0
  269. package/src/adapter/bots.ts +582 -0
  270. package/src/adapter/host.ts +224 -0
  271. package/src/adapter/login.ts +481 -0
  272. package/src/adapter/registry.ts +202 -0
  273. package/src/config/core-config.test.ts +60 -0
  274. package/src/config/core-config.ts +401 -0
  275. package/src/config/schema.test.ts +179 -0
  276. package/src/config/schema.ts +1036 -0
  277. package/src/config/store.test.ts +245 -0
  278. package/src/config/store.ts +506 -0
  279. package/src/config/yaml.ts +119 -0
  280. package/src/http/client.test.ts +568 -0
  281. package/src/http/client.ts +777 -0
  282. package/src/index.ts +106 -0
  283. package/src/kernel/app.test.ts +267 -0
  284. package/src/kernel/app.ts +843 -0
  285. package/src/kernel/kv-sink.ts +58 -0
  286. package/src/kernel/policy.ts +120 -0
  287. package/src/kernel/runtime.test.ts +542 -0
  288. package/src/kernel/runtime.ts +307 -0
  289. package/src/kernel/sql-sink.ts +154 -0
  290. package/src/logger/format.ts +259 -0
  291. package/src/logger/index.ts +427 -0
  292. package/src/logger/rotate.ts +265 -0
  293. package/src/message/segment.test.ts +335 -0
  294. package/src/message/segment.ts +569 -0
  295. package/src/message/split.ts +97 -0
  296. package/src/message/target.ts +48 -0
  297. package/src/pipeline/cooldown.ts +136 -0
  298. package/src/pipeline/dispatch.ts +419 -0
  299. package/src/pipeline/event.ts +735 -0
  300. package/src/pipeline/middleware.test.ts +426 -0
  301. package/src/pipeline/middleware.ts +199 -0
  302. package/src/pipeline/prompt.ts +212 -0
  303. package/src/pipeline/router.test.ts +432 -0
  304. package/src/pipeline/router.ts +575 -0
  305. package/src/platform/detect.ts +178 -0
  306. package/src/platform/paths.ts +147 -0
  307. package/src/platform/system.test.ts +102 -0
  308. package/src/platform/system.ts +289 -0
  309. package/src/plugin/context.test.ts +459 -0
  310. package/src/plugin/context.ts +757 -0
  311. package/src/plugin/define.test.ts +120 -0
  312. package/src/plugin/define.ts +142 -0
  313. package/src/plugin/discover.test.ts +253 -0
  314. package/src/plugin/discover.ts +353 -0
  315. package/src/plugin/events.test.ts +162 -0
  316. package/src/plugin/events.ts +266 -0
  317. package/src/plugin/hooks.ts +373 -0
  318. package/src/plugin/host.test.ts +696 -0
  319. package/src/plugin/host.ts +738 -0
  320. package/src/plugin/market.test.ts +781 -0
  321. package/src/plugin/market.ts +870 -0
  322. package/src/plugin/services.test.ts +135 -0
  323. package/src/plugin/services.ts +261 -0
  324. package/src/plugin/tar.test.ts +158 -0
  325. package/src/plugin/tar.ts +309 -0
  326. package/src/render/registry.test.ts +127 -0
  327. package/src/render/registry.ts +439 -0
  328. package/src/scheduler/index.ts +357 -0
  329. package/src/server/api.test.ts +802 -0
  330. package/src/server/api.ts +869 -0
  331. package/src/server/auth.ts +201 -0
  332. package/src/server/browse.test.ts +193 -0
  333. package/src/server/browse.ts +240 -0
  334. package/src/server/files.ts +97 -0
  335. package/src/server/index.test.ts +566 -0
  336. package/src/server/index.ts +1386 -0
  337. package/src/server/table.ts +351 -0
  338. package/src/store/index.ts +156 -0
  339. package/src/store/json.ts +198 -0
  340. package/src/store/kv.test.ts +307 -0
  341. package/src/store/kv.ts +256 -0
  342. package/src/store/level.ts +167 -0
  343. package/src/store/memory.ts +109 -0
  344. package/src/store/sql.test.ts +204 -0
  345. package/src/store/sql.ts +324 -0
  346. package/src/testing/fake.ts +297 -0
  347. package/src/testing/index.ts +15 -0
  348. package/src/testing/mock-adapter.ts +588 -0
  349. package/src/util/deep.ts +272 -0
  350. package/src/util/defer.ts +211 -0
  351. package/src/util/dispose.ts +169 -0
  352. package/src/util/duration.ts +82 -0
  353. package/src/util/fs.ts +279 -0
  354. package/src/util/id.ts +94 -0
  355. package/src/util/lru.ts +185 -0
  356. package/src/util/queue.ts +167 -0
  357. 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
+ })