dsh-plugin-compat-check 0.1.6 → 0.1.7
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 +17 -30
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/index.js +1295 -550
- package/kernel/lib/api-semantics.mjs +223 -0
- package/kernel/lib/author-ci-budget.mjs +25 -0
- package/kernel/lib/author-ci-output.mjs +22 -0
- package/kernel/lib/author-ci-policy.mjs +6 -1
- package/kernel/lib/baseline.mjs +109 -38
- package/kernel/lib/catalog.mjs +11 -3
- package/kernel/lib/contracts.mjs +16 -1
- package/kernel/lib/execution-environment.mjs +52 -0
- package/kernel/lib/extract.mjs +57 -13
- package/kernel/lib/finding-guidance.mjs +6 -0
- package/kernel/lib/finding-review-store.mjs +11 -23
- package/kernel/lib/issue-report.mjs +2 -1
- package/kernel/lib/patch-mounts.mjs +232 -0
- package/kernel/lib/report-identity-worker.mjs +47 -26
- package/kernel/lib/report-identity.mjs +4 -3
- package/kernel/lib/report-service.mjs +118 -38
- package/kernel/lib/report.mjs +13 -17
- package/kernel/lib/source-analysis.mjs +258 -42
- package/kernel/lib/source-graph.mjs +353 -75
- package/kernel/lib/state-lock.mjs +206 -0
- package/kernel/lib/state-store.mjs +41 -30
- package/kernel/lib/static-input.mjs +111 -0
- package/kernel/lib/syntax.mjs +242 -20
- package/kernel/lib/upgrade-contract.mjs +1 -0
- package/kernel/lib/upgrade-input.mjs +9 -4
- package/kernel/lib/upgrade-preview.mjs +11 -15
- package/kernel/lib/upgrade-worker.mjs +10 -1
- package/kernel/lib/verdict.mjs +28 -5
- package/kernel/lib/version.mjs +2 -34
- package/kernel/lib/worker-analysis.mjs +74 -3
- package/kernel/rules/README.md +30 -208
- package/kernel/rules/dsh-0.2.1-rc.3-review.json +63 -0
- package/kernel/rules/dsh-api-catalog-0.2.1-rc.3.json +743 -0
- package/kernel/rules/severity.json +77 -0
- package/lib/client.js +4668 -3423
- package/lib/finding-guidance.js +6 -0
- package/lib/local-request.js +44 -0
- package/lib/report-contract.js +16 -1
- package/lib/upgrade-contract.js +1 -0
- package/package.json +3 -2
- package/scripts/prepublish-check.mjs +1 -22
- package/scripts/stage-contract.mjs +7 -0
- package/skill/SKILL.md +13 -12
package/kernel/lib/extract.mjs
CHANGED
|
@@ -3,6 +3,12 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { analyzeSources } from "./source-analysis.mjs";
|
|
6
|
+
import {
|
|
7
|
+
readStaticFile,
|
|
8
|
+
readStaticDirectory,
|
|
9
|
+
inputError,
|
|
10
|
+
lowerInputLimits,
|
|
11
|
+
} from "./static-input.mjs";
|
|
6
12
|
import {
|
|
7
13
|
collectPackageSources,
|
|
8
14
|
clientEntry as declaredClientEntry,
|
|
@@ -14,12 +20,19 @@ const cmp = (a, b) =>
|
|
|
14
20
|
: String(a ?? "") > String(b ?? "")
|
|
15
21
|
? 1
|
|
16
22
|
: 0;
|
|
17
|
-
const pkgJson = (dir) => {
|
|
23
|
+
const pkgJson = (dir, maxBytes, check) => {
|
|
24
|
+
let readBytes = 0;
|
|
18
25
|
try {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
const bytes = readStaticFile(join(dir, "package.json"), maxBytes, check);
|
|
27
|
+
readBytes = bytes.length;
|
|
28
|
+
const p = JSON.parse(bytes.toString("utf8"));
|
|
29
|
+
if (!p || typeof p !== "object" || Array.isArray(p))
|
|
30
|
+
throw new Error("Invalid package object");
|
|
31
|
+
return { meta: p, bytes: bytes.length };
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR") return {};
|
|
34
|
+
if (error.code === "STATIC_INVENTORY_BUDGET") throw error;
|
|
35
|
+
return { error, bytes: readBytes };
|
|
23
36
|
}
|
|
24
37
|
};
|
|
25
38
|
const sortCalls = (calls) =>
|
|
@@ -45,7 +58,7 @@ function clientHalfDeliverable(pkgDir, j) {
|
|
|
45
58
|
platform: "web",
|
|
46
59
|
ok: false,
|
|
47
60
|
declaredEntry: null,
|
|
48
|
-
why: '声明了 dsh.client
|
|
61
|
+
why: '声明了 dsh.client,但 exports["./client"] 不是字符串或含字符串 default 的对象',
|
|
49
62
|
};
|
|
50
63
|
try {
|
|
51
64
|
const root = fs.realpathSync(pkgDir),
|
|
@@ -96,14 +109,37 @@ export function extractWithProvenance(nm, options = {}) {
|
|
|
96
109
|
// 找出「社区插件」:package.json 里有 dsh 清单、且非 @deepseek-ai 官方
|
|
97
110
|
const inventoryDiagnostics = [];
|
|
98
111
|
const targets = [];
|
|
99
|
-
|
|
112
|
+
const inventory = lowerInputLimits(
|
|
113
|
+
{
|
|
114
|
+
entries: 50000,
|
|
115
|
+
bytes: 32 * 1024 * 1024,
|
|
116
|
+
manifestBytes: 65536,
|
|
117
|
+
timeMs: 5000,
|
|
118
|
+
},
|
|
119
|
+
options.inventoryLimits,
|
|
120
|
+
);
|
|
121
|
+
let inventoryEntries = 0,
|
|
122
|
+
inventoryBytes = 0;
|
|
123
|
+
const started = Date.now();
|
|
124
|
+
const checkInventory = () => {
|
|
125
|
+
if (Date.now() - started > inventory.timeMs)
|
|
126
|
+
throw inputError("STATIC_INVENTORY_BUDGET", "插件清单超过时间预算");
|
|
127
|
+
};
|
|
128
|
+
const listInventory = (dir) => {
|
|
129
|
+
const names = readStaticDirectory(
|
|
130
|
+
dir,
|
|
131
|
+
inventory.entries - inventoryEntries,
|
|
132
|
+
checkInventory,
|
|
133
|
+
);
|
|
134
|
+
inventoryEntries += names.length;
|
|
135
|
+
return names;
|
|
136
|
+
};
|
|
137
|
+
for (const entry of listInventory(nm)) {
|
|
138
|
+
checkInventory();
|
|
100
139
|
const names = entry.startsWith("@")
|
|
101
140
|
? (() => {
|
|
102
141
|
try {
|
|
103
|
-
return
|
|
104
|
-
.readdirSync(join(nm, entry))
|
|
105
|
-
.sort()
|
|
106
|
-
.map((s) => join(entry, s));
|
|
142
|
+
return listInventory(join(nm, entry)).map((s) => join(entry, s));
|
|
107
143
|
} catch (error) {
|
|
108
144
|
throw new Error(
|
|
109
145
|
`无法读取插件 scope ${entry}:${error.code || error.message}`,
|
|
@@ -112,12 +148,20 @@ export function extractWithProvenance(nm, options = {}) {
|
|
|
112
148
|
})()
|
|
113
149
|
: [entry];
|
|
114
150
|
for (const base of names) {
|
|
151
|
+
checkInventory();
|
|
115
152
|
const full = join(nm, base);
|
|
116
|
-
const
|
|
117
|
-
|
|
153
|
+
const metadata = pkgJson(
|
|
154
|
+
full,
|
|
155
|
+
Math.min(inventory.manifestBytes, inventory.bytes - inventoryBytes),
|
|
156
|
+
checkInventory,
|
|
157
|
+
);
|
|
158
|
+
inventoryBytes += metadata.bytes || 0;
|
|
159
|
+
const j = metadata.meta;
|
|
160
|
+
if (metadata.error)
|
|
118
161
|
inventoryDiagnostics.push({
|
|
119
162
|
kind: "invalid-package-metadata",
|
|
120
163
|
file: base,
|
|
164
|
+
code: metadata.error.code || "STATIC_INPUT_INVALID",
|
|
121
165
|
});
|
|
122
166
|
if (
|
|
123
167
|
!j ||
|
|
@@ -52,6 +52,12 @@ export function nextSteps(finding) {
|
|
|
52
52
|
"在报告给出的源码位置核对服务读取及必需 inject 的作用域,并确认 absent 观测绑定同一 profile、DSH 版本与采样时间。",
|
|
53
53
|
"核对该服务的实际提供者及 loader 状态,补充相关功能操作结果;服务缺失的推断影响不等于业务失败已实测。",
|
|
54
54
|
];
|
|
55
|
+
case "api-argument-invalid":
|
|
56
|
+
case "api-option-unsupported":
|
|
57
|
+
return [
|
|
58
|
+
"按源码位置核对选项类型、枚举、实际传值和精确目标版本,查阅 evidence 中固定源码及归档摘要;方法存在或能力守卫不证明参数有效。",
|
|
59
|
+
"在独立目标会话核验草稿保留、替换或追加;本项仅是静态参数风险,不证明业务失败,也不自动修改或提交草稿。",
|
|
60
|
+
];
|
|
55
61
|
case "host-method-missing":
|
|
56
62
|
return [
|
|
57
63
|
"在报告给出的文件与行号核对实际方法调用、Context 绑定及能力守卫,检查是否存在报告尚未识别的兜底。",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Bounded manual-review sidecar storage. Never mutates reports or baseline state. */
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import { acquireStateLock, stateDirectory } from "./state-lock.mjs";
|
|
3
4
|
import { constants } from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import os from "node:os";
|
|
@@ -105,16 +106,13 @@ function storage(context, options) {
|
|
|
105
106
|
`reviews.${context.targetKey}.json`,
|
|
106
107
|
);
|
|
107
108
|
}
|
|
108
|
-
async function safeDirectory(dir,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)
|
|
116
|
-
throw fault("Unsafe review directory", "REVIEW_STORAGE");
|
|
117
|
-
if (create && stat.mode & 0o077) await fs.chmod(dir, 0o700);
|
|
109
|
+
async function safeDirectory(dir, options = {}) {
|
|
110
|
+
try {
|
|
111
|
+
stateDirectory(dir, options);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error.code === "ENOENT") throw error;
|
|
114
|
+
throw fault(error.message, "REVIEW_STORAGE");
|
|
115
|
+
}
|
|
118
116
|
}
|
|
119
117
|
async function load(file) {
|
|
120
118
|
try {
|
|
@@ -185,18 +183,11 @@ export async function saveReview(context, input, options = {}) {
|
|
|
185
183
|
let lockHandle = null;
|
|
186
184
|
try {
|
|
187
185
|
try {
|
|
188
|
-
lockHandle =
|
|
189
|
-
lock,
|
|
190
|
-
constants.O_CREAT |
|
|
191
|
-
constants.O_EXCL |
|
|
192
|
-
constants.O_WRONLY |
|
|
193
|
-
constants.O_NOFOLLOW,
|
|
194
|
-
0o600,
|
|
195
|
-
);
|
|
186
|
+
lockHandle = acquireStateLock(lock);
|
|
196
187
|
} catch (error) {
|
|
197
188
|
if (error.code === "EEXIST")
|
|
198
189
|
throw fault(
|
|
199
|
-
`人工处置写入锁仍存在或上次保存已中断:${lock}
|
|
190
|
+
`人工处置写入锁仍存在或上次保存已中断:${lock}。刷新不会清除此锁;使用对应源码仓库的 bin/recover-lock.mjs 检查并显式恢复已确认退出的持有者;保留原始 JSON 及临时文件。`,
|
|
200
191
|
"REVIEW_BUSY",
|
|
201
192
|
);
|
|
202
193
|
throw error;
|
|
@@ -237,9 +228,6 @@ export async function saveReview(context, input, options = {}) {
|
|
|
237
228
|
return combineReviews(context, records, { revision: next.revision });
|
|
238
229
|
} finally {
|
|
239
230
|
await fs.rm(temp, { force: true }).catch(() => {});
|
|
240
|
-
|
|
241
|
-
await lockHandle.close();
|
|
242
|
-
await fs.unlink(lock).catch(() => {});
|
|
243
|
-
}
|
|
231
|
+
lockHandle?.release();
|
|
244
232
|
}
|
|
245
233
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/** Shared CLI/panel share view. Never serialize raw inputs, logs, evidence or baseline. */
|
|
2
2
|
import { TOOL_VERSION } from "./version.mjs";
|
|
3
3
|
import { nextSteps } from "./finding-guidance.mjs";
|
|
4
|
+
import { terminalText } from "./contracts.mjs";
|
|
4
5
|
|
|
5
6
|
function safe(value) {
|
|
6
7
|
const raw = String(value ?? "未提供");
|
|
7
8
|
// Hide the whole field: token-by-token replacement leaks the tail of a path
|
|
8
9
|
// containing spaces and can turn a multi-line log into apparently safe prose.
|
|
9
10
|
if (
|
|
10
|
-
|
|
11
|
+
terminalText(raw) !== raw ||
|
|
11
12
|
/(?:[a-z]:[\\/]|\\\\|~[\\/]|(?:^|[\s("'`=::,;])\/|\.\.[\\/]|\/[^\s/]+\/)/i.test(
|
|
12
13
|
raw,
|
|
13
14
|
) ||
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/** Bounded, non-evaluating subset of DSH's ordered bundle patch composition.
|
|
2
|
+
* Unsupported expressions/aliases and cross-bundle targets limit coverage.
|
|
3
|
+
* Only insert rows (and known group descendants) introduce module roots.
|
|
4
|
+
*/
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import { parseDocument, isAlias, isMap, isSeq, isPair } from "yaml";
|
|
8
|
+
import { inputError } from "./static-input.mjs";
|
|
9
|
+
|
|
10
|
+
export function patchMounts(files, { root, read, check, note, limits }) {
|
|
11
|
+
const rows = [],
|
|
12
|
+
index = new Map(),
|
|
13
|
+
bases = new WeakMap(),
|
|
14
|
+
assertionNames = new WeakMap();
|
|
15
|
+
let nodes = 0,
|
|
16
|
+
complete = true;
|
|
17
|
+
const uncertain = (file, message) => {
|
|
18
|
+
complete = false;
|
|
19
|
+
note("patch-resolution-uncertain", file, message);
|
|
20
|
+
};
|
|
21
|
+
const invalid = (message) => {
|
|
22
|
+
throw new Error(message);
|
|
23
|
+
};
|
|
24
|
+
const inspect = (root) => {
|
|
25
|
+
const queue = [[root, 0]];
|
|
26
|
+
while (queue.length) {
|
|
27
|
+
check();
|
|
28
|
+
const [node, depth] = queue.pop();
|
|
29
|
+
if (!node || typeof node !== "object") continue;
|
|
30
|
+
if (++nodes > limits.yamlNodes || depth > limits.yamlDepth)
|
|
31
|
+
throw inputError(
|
|
32
|
+
"STATIC_INPUT_BUDGET",
|
|
33
|
+
"Patch node/depth budget exceeded",
|
|
34
|
+
);
|
|
35
|
+
// Zero alias expansion: anchors alone are harmless, aliases are explicit
|
|
36
|
+
// unsupported input. No cyclic graph or expansion reaches composition.
|
|
37
|
+
if (isAlias(node))
|
|
38
|
+
invalid("Patch aliases are unsupported (expansion budget 0)");
|
|
39
|
+
if (isMap(node) || isSeq(node))
|
|
40
|
+
for (const item of node.items) queue.push([item, depth + 1]);
|
|
41
|
+
else if (isPair(node)) {
|
|
42
|
+
queue.push([node.key, depth + 1], [node.value, depth + 1]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const rowList = (entries, base) => {
|
|
47
|
+
if (!Array.isArray(entries))
|
|
48
|
+
invalid("Patch insert/group config must be a literal row list");
|
|
49
|
+
for (const row of entries) {
|
|
50
|
+
if (!(row instanceof Map)) invalid("Patch row must be a literal mapping");
|
|
51
|
+
for (const key of ["group", "disabled"])
|
|
52
|
+
if (
|
|
53
|
+
row.has(key) &&
|
|
54
|
+
row.get(key) !== null &&
|
|
55
|
+
typeof row.get(key) !== "boolean"
|
|
56
|
+
)
|
|
57
|
+
invalid("Patch row flags must be literal booleans");
|
|
58
|
+
if (typeof row.get("name") !== "string" || !row.get("name"))
|
|
59
|
+
invalid("Patch row name is unavailable");
|
|
60
|
+
if (row.has("id") && typeof row.get("id") !== "string")
|
|
61
|
+
invalid("Patch row id must be a string");
|
|
62
|
+
bases.set(row, base);
|
|
63
|
+
const name = row.get("name");
|
|
64
|
+
assertionNames.set(
|
|
65
|
+
row,
|
|
66
|
+
path.isAbsolute(name) || name.startsWith("./") || name.startsWith("../")
|
|
67
|
+
? pathToFileURL(path.resolve(root, base, name)).href
|
|
68
|
+
: name,
|
|
69
|
+
);
|
|
70
|
+
const id = row.get("id");
|
|
71
|
+
if (id) {
|
|
72
|
+
if (index.has(id)) invalid("Duplicate patch row id is ambiguous");
|
|
73
|
+
index.set(id, row);
|
|
74
|
+
}
|
|
75
|
+
if (row.get("group") && Array.isArray(row.get("config")))
|
|
76
|
+
rowList(row.get("config"), base);
|
|
77
|
+
}
|
|
78
|
+
return entries;
|
|
79
|
+
};
|
|
80
|
+
for (let i = 0; i < files.length; i++) {
|
|
81
|
+
const file = files[i];
|
|
82
|
+
try {
|
|
83
|
+
check();
|
|
84
|
+
if (i >= limits.yamlFiles)
|
|
85
|
+
throw inputError(
|
|
86
|
+
"STATIC_INPUT_BUDGET",
|
|
87
|
+
"Patch file count budget exceeded",
|
|
88
|
+
);
|
|
89
|
+
const doc = parseDocument(read(file), {
|
|
90
|
+
uniqueKeys: true,
|
|
91
|
+
schema: "core",
|
|
92
|
+
});
|
|
93
|
+
check();
|
|
94
|
+
if (doc.errors.length || doc.warnings.length)
|
|
95
|
+
invalid("Invalid or unsupported patch YAML");
|
|
96
|
+
inspect(doc.contents);
|
|
97
|
+
const patches = doc.toJS({ mapAsMap: true, maxAliasCount: 0 });
|
|
98
|
+
if (!Array.isArray(patches)) invalid("Patch must be a top-level list");
|
|
99
|
+
for (const patch of patches) {
|
|
100
|
+
check();
|
|
101
|
+
if (!(patch instanceof Map))
|
|
102
|
+
invalid("Patch operation must be a mapping");
|
|
103
|
+
const id = patch.get("id"),
|
|
104
|
+
insert = patch.get("insert");
|
|
105
|
+
if (patch.has("insert")) {
|
|
106
|
+
const target = id ? index.get(id) : null;
|
|
107
|
+
if (id && (!target || !target.get("group"))) {
|
|
108
|
+
uncertain(file, "Insert target is unavailable or not a group");
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const added = rowList(insert, path.dirname(file));
|
|
112
|
+
if (target) {
|
|
113
|
+
if (!Array.isArray(target.get("config"))) target.set("config", []);
|
|
114
|
+
target.get("config").push(...added);
|
|
115
|
+
} else rows.push(...added);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const target = id && index.get(id);
|
|
119
|
+
if (!target) {
|
|
120
|
+
uncertain(
|
|
121
|
+
file,
|
|
122
|
+
"Override target is unavailable; name is an assertion, not a mount",
|
|
123
|
+
);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (
|
|
127
|
+
patch.get("name") &&
|
|
128
|
+
patch.get("name") !== assertionNames.get(target)
|
|
129
|
+
) {
|
|
130
|
+
uncertain(file, "Override name assertion did not match");
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
for (const [key, value] of patch) {
|
|
134
|
+
if (["id", "name"].includes(key)) continue;
|
|
135
|
+
if (
|
|
136
|
+
["group", "disabled"].includes(key) &&
|
|
137
|
+
value !== null &&
|
|
138
|
+
typeof value !== "boolean"
|
|
139
|
+
)
|
|
140
|
+
invalid("Dynamic patch flags cannot establish mounts");
|
|
141
|
+
target.set(key, value);
|
|
142
|
+
}
|
|
143
|
+
// Like the host, config replacements do not reindex new child ids.
|
|
144
|
+
// Relative paths in such replacements use the profile tree, not the
|
|
145
|
+
// patch file's base, so they must not be guessed package-relative.
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
complete = false;
|
|
149
|
+
note(
|
|
150
|
+
error.code === "STATIC_INPUT_BUDGET"
|
|
151
|
+
? "scan-budget-exceeded"
|
|
152
|
+
: "patch-unreadable",
|
|
153
|
+
file,
|
|
154
|
+
error.message,
|
|
155
|
+
);
|
|
156
|
+
return { mounts: [], complete: false };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const mounts = [];
|
|
160
|
+
const walk = (entries, disabled = false) => {
|
|
161
|
+
for (const row of entries) {
|
|
162
|
+
check();
|
|
163
|
+
if (!(row instanceof Map)) {
|
|
164
|
+
uncertain(null, "Group replacement contains an invalid row");
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const name = row.get("name"),
|
|
168
|
+
group = row.get("group") === true;
|
|
169
|
+
if (
|
|
170
|
+
["group", "disabled"].some(
|
|
171
|
+
(key) =>
|
|
172
|
+
row.has(key) &&
|
|
173
|
+
row.get(key) !== null &&
|
|
174
|
+
typeof row.get(key) !== "boolean",
|
|
175
|
+
)
|
|
176
|
+
) {
|
|
177
|
+
uncertain(null, "Replacement row flags are not literal booleans");
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const blocked = disabled || row.get("disabled") === true;
|
|
181
|
+
if (!group && blocked) continue;
|
|
182
|
+
if (typeof name !== "string") {
|
|
183
|
+
uncertain(null, "Group replacement name is unavailable");
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (name === "cordis:group" && group) {
|
|
187
|
+
const children = row.get("config");
|
|
188
|
+
if (!Array.isArray(children))
|
|
189
|
+
uncertain(null, "Group config is not a literal row list");
|
|
190
|
+
else walk(children, blocked);
|
|
191
|
+
} else if (name.startsWith("cordis:")) {
|
|
192
|
+
uncertain(null, "Builtin subtree semantics are not established");
|
|
193
|
+
} else {
|
|
194
|
+
if (path.isAbsolute(name)) mounts.push(name);
|
|
195
|
+
else if (name.startsWith("file://")) {
|
|
196
|
+
try {
|
|
197
|
+
const url = new URL(name);
|
|
198
|
+
if (url.search || url.hash)
|
|
199
|
+
throw Error("URL modifiers are not established");
|
|
200
|
+
mounts.push(fileURLToPath(url));
|
|
201
|
+
} catch {
|
|
202
|
+
uncertain(null, "File URL mount is not established");
|
|
203
|
+
}
|
|
204
|
+
} else if (name.startsWith("./") || name.startsWith("../")) {
|
|
205
|
+
if (!bases.has(row))
|
|
206
|
+
uncertain(
|
|
207
|
+
null,
|
|
208
|
+
"Relative replacement row depends on the profile base",
|
|
209
|
+
);
|
|
210
|
+
else mounts.push("./" + path.join(bases.get(row), name));
|
|
211
|
+
} else if (name.startsWith("."))
|
|
212
|
+
uncertain(null, "Relative mount depends on the profile base");
|
|
213
|
+
else mounts.push(name);
|
|
214
|
+
if (group)
|
|
215
|
+
uncertain(null, "Custom group descendants are not established");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
try {
|
|
220
|
+
walk(rows);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
note(
|
|
223
|
+
error.code === "STATIC_INPUT_BUDGET"
|
|
224
|
+
? "scan-budget-exceeded"
|
|
225
|
+
: "patch-unreadable",
|
|
226
|
+
null,
|
|
227
|
+
error.message,
|
|
228
|
+
);
|
|
229
|
+
return { mounts: [], complete: false };
|
|
230
|
+
}
|
|
231
|
+
return { mounts, complete };
|
|
232
|
+
}
|
|
@@ -2,13 +2,40 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { parentPort, workerData } from "node:worker_threads";
|
|
5
|
+
import {
|
|
6
|
+
readStaticDirectory,
|
|
7
|
+
readStaticFile,
|
|
8
|
+
lowerInputLimits,
|
|
9
|
+
} from "./static-input.mjs";
|
|
10
|
+
const limits = lowerInputLimits(
|
|
11
|
+
{
|
|
12
|
+
entries: 50000,
|
|
13
|
+
files: 50000,
|
|
14
|
+
directories: 10000,
|
|
15
|
+
bytes: 32 * 1024 * 1024,
|
|
16
|
+
timeMs: 5000,
|
|
17
|
+
},
|
|
18
|
+
workerData.limits,
|
|
19
|
+
);
|
|
20
|
+
const started = Date.now();
|
|
21
|
+
const check = () => {
|
|
22
|
+
if (Date.now() - started > limits.timeMs) throw Error("缓存身份时间预算超限");
|
|
23
|
+
};
|
|
24
|
+
let entries = 0,
|
|
25
|
+
bytesRead = 0;
|
|
26
|
+
const list = (dir) => {
|
|
27
|
+
const names = readStaticDirectory(dir, limits.entries - entries, check);
|
|
28
|
+
entries += names.length;
|
|
29
|
+
return names;
|
|
30
|
+
};
|
|
5
31
|
const hash = createHash("sha256");
|
|
6
32
|
const add = (value) => hash.update(JSON.stringify(value) + "\n");
|
|
7
|
-
let remaining =
|
|
8
|
-
directories =
|
|
33
|
+
let remaining = limits.files,
|
|
34
|
+
directories = limits.directories;
|
|
9
35
|
const services = new Set(),
|
|
10
36
|
manifests = [];
|
|
11
37
|
function metadata(file) {
|
|
38
|
+
check();
|
|
12
39
|
if (--remaining < 0) throw Error("缓存身份文件预算超限,等待后续刷新");
|
|
13
40
|
const stat = fs.statSync(file);
|
|
14
41
|
add([
|
|
@@ -23,32 +50,29 @@ function metadata(file) {
|
|
|
23
50
|
return stat;
|
|
24
51
|
}
|
|
25
52
|
function smallFile(file, limit = 1024 * 1024) {
|
|
26
|
-
let fd;
|
|
27
53
|
try {
|
|
28
54
|
metadata(file);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
let bytes;
|
|
56
|
+
try {
|
|
57
|
+
bytes = readStaticFile(
|
|
58
|
+
file,
|
|
59
|
+
Math.min(limit, limits.bytes - bytesRead),
|
|
60
|
+
check,
|
|
61
|
+
);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error.code === "STATIC_INPUT_TYPE")
|
|
64
|
+
throw Error("缓存身份输入不是预算内普通文件");
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
bytesRead += bytes.length;
|
|
42
68
|
add(bytes.toString("base64"));
|
|
43
69
|
return bytes;
|
|
44
70
|
} catch (error) {
|
|
45
|
-
if (
|
|
71
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code)) {
|
|
46
72
|
add([file, "missing"]);
|
|
47
73
|
return null;
|
|
48
74
|
}
|
|
49
75
|
throw error;
|
|
50
|
-
} finally {
|
|
51
|
-
if (fd !== undefined) fs.closeSync(fd);
|
|
52
76
|
}
|
|
53
77
|
}
|
|
54
78
|
|
|
@@ -66,7 +90,7 @@ function walk(root) {
|
|
|
66
90
|
)
|
|
67
91
|
continue;
|
|
68
92
|
seen.add(real);
|
|
69
|
-
for (const entry of
|
|
93
|
+
for (const entry of list(dir)) {
|
|
70
94
|
const file = path.join(dir, entry);
|
|
71
95
|
let stat;
|
|
72
96
|
try {
|
|
@@ -86,12 +110,9 @@ try {
|
|
|
86
110
|
const { nodeModules, files = [], context = {} } = workerData;
|
|
87
111
|
add(context);
|
|
88
112
|
metadata(nodeModules);
|
|
89
|
-
for (const name of
|
|
113
|
+
for (const name of list(nodeModules)) {
|
|
90
114
|
const bases = name.startsWith("@")
|
|
91
|
-
?
|
|
92
|
-
.readdirSync(path.join(nodeModules, name))
|
|
93
|
-
.sort()
|
|
94
|
-
.map((n) => path.join(name, n))
|
|
115
|
+
? list(path.join(nodeModules, name)).map((n) => path.join(name, n))
|
|
95
116
|
: [name];
|
|
96
117
|
for (const base of bases) {
|
|
97
118
|
const root = path.join(nodeModules, base);
|
|
@@ -99,7 +120,7 @@ try {
|
|
|
99
120
|
try {
|
|
100
121
|
bytes = smallFile(path.join(root, "package.json"), 65536);
|
|
101
122
|
} catch (error) {
|
|
102
|
-
if (remaining < 0) throw error;
|
|
123
|
+
if (remaining < 0 || error.code === "STATIC_INPUT_BUDGET") throw error;
|
|
103
124
|
add([root, "unreadable-manifest", error.code || error.message]);
|
|
104
125
|
continue;
|
|
105
126
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { runReportWorkerTask } from "./worker-analysis.mjs";
|
|
2
2
|
/** Bounded metadata walk runs off the host thread and never imports plugin code. */
|
|
3
|
-
export function reportIdentity(inputs) {
|
|
4
|
-
return
|
|
3
|
+
export function reportIdentity(inputs, { signal } = {}) {
|
|
4
|
+
return runReportWorkerTask(
|
|
5
5
|
new URL("./report-identity-worker.mjs", import.meta.url),
|
|
6
6
|
inputs,
|
|
7
7
|
{
|
|
8
8
|
timeoutMs: 5000,
|
|
9
|
+
signal,
|
|
9
10
|
resourceLimits: {
|
|
10
11
|
maxOldGenerationSizeMb: 128,
|
|
11
12
|
maxYoungGenerationSizeMb: 16,
|