@getmarrow/mcp 3.9.102 → 3.9.106
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 +71 -24
- package/dist/approval-reply.d.ts +2 -0
- package/dist/approval-reply.d.ts.map +1 -0
- package/dist/approval-reply.js +18 -0
- package/dist/approval-reply.js.map +1 -0
- package/dist/cli.js +1716 -1644
- package/dist/cli.js.map +1 -1
- package/dist/codex-plugin-pins.d.ts +19 -0
- package/dist/codex-plugin-pins.d.ts.map +1 -0
- package/dist/codex-plugin-pins.js +61 -0
- package/dist/codex-plugin-pins.js.map +1 -0
- package/dist/codex-plugins.d.ts +34 -0
- package/dist/codex-plugins.d.ts.map +1 -0
- package/dist/codex-plugins.js +240 -0
- package/dist/codex-plugins.js.map +1 -0
- package/dist/env.d.ts +8 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +83 -4
- package/dist/env.js.map +1 -1
- package/dist/hook-context.d.ts.map +1 -1
- package/dist/hook-context.js +25 -0
- package/dist/hook-context.js.map +1 -1
- package/dist/hook-contract.d.ts +4 -3
- package/dist/hook-contract.d.ts.map +1 -1
- package/dist/hook-contract.js +43 -5
- package/dist/hook-contract.js.map +1 -1
- package/dist/hook-pre-action.d.ts +80 -3
- package/dist/hook-pre-action.d.ts.map +1 -1
- package/dist/hook-pre-action.js +435 -29
- package/dist/hook-pre-action.js.map +1 -1
- package/dist/hook-tool-policy.d.ts +73 -4
- package/dist/hook-tool-policy.d.ts.map +1 -1
- package/dist/hook-tool-policy.js +287 -18
- package/dist/hook-tool-policy.js.map +1 -1
- package/dist/hook.d.ts +1 -1
- package/dist/hook.d.ts.map +1 -1
- package/dist/hook.js +15 -7
- package/dist/hook.js.map +1 -1
- package/dist/host-approval-state.d.ts +7 -0
- package/dist/host-approval-state.d.ts.map +1 -1
- package/dist/host-approval-state.js +15 -0
- package/dist/host-approval-state.js.map +1 -1
- package/dist/host-approval.d.ts +22 -1
- package/dist/host-approval.d.ts.map +1 -1
- package/dist/host-approval.js +65 -4
- package/dist/host-approval.js.map +1 -1
- package/dist/host-session.d.ts +11 -0
- package/dist/host-session.d.ts.map +1 -1
- package/dist/host-session.js +49 -0
- package/dist/host-session.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -11
- package/dist/index.js.map +1 -1
- package/dist/keyless-server.d.ts +53 -0
- package/dist/keyless-server.d.ts.map +1 -0
- package/dist/keyless-server.js +230 -0
- package/dist/keyless-server.js.map +1 -0
- package/dist/normalized-action.d.ts +31 -6
- package/dist/normalized-action.d.ts.map +1 -1
- package/dist/normalized-action.js +550 -105
- package/dist/normalized-action.js.map +1 -1
- package/dist/own-server-identity.d.ts +142 -0
- package/dist/own-server-identity.d.ts.map +1 -0
- package/dist/own-server-identity.js +1123 -0
- package/dist/own-server-identity.js.map +1 -0
- package/dist/own-tool.d.ts +21 -9
- package/dist/own-tool.d.ts.map +1 -1
- package/dist/own-tool.js +25 -10
- package/dist/own-tool.js.map +1 -1
- package/dist/redact.d.ts +23 -0
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +181 -8
- package/dist/redact.js.map +1 -1
- package/dist/seatbelt/classifier-pin.d.ts +15 -0
- package/dist/seatbelt/classifier-pin.d.ts.map +1 -0
- package/dist/seatbelt/classifier-pin.js +26 -0
- package/dist/seatbelt/classifier-pin.js.map +1 -0
- package/dist/seatbelt/env-dump.d.ts +45 -0
- package/dist/seatbelt/env-dump.d.ts.map +1 -0
- package/dist/seatbelt/env-dump.js +308 -0
- package/dist/seatbelt/env-dump.js.map +1 -0
- package/dist/seatbelt/fallback-copy.d.ts +16 -0
- package/dist/seatbelt/fallback-copy.d.ts.map +1 -0
- package/dist/seatbelt/fallback-copy.js +68 -0
- package/dist/seatbelt/fallback-copy.js.map +1 -0
- package/dist/seatbelt/guards.d.ts +10 -0
- package/dist/seatbelt/guards.d.ts.map +1 -0
- package/dist/seatbelt/guards.js +509 -0
- package/dist/seatbelt/guards.js.map +1 -0
- package/dist/seatbelt/hook.d.ts +91 -0
- package/dist/seatbelt/hook.d.ts.map +1 -0
- package/dist/seatbelt/hook.js +396 -0
- package/dist/seatbelt/hook.js.map +1 -0
- package/dist/seatbelt/longopts.d.ts +68 -0
- package/dist/seatbelt/longopts.d.ts.map +1 -0
- package/dist/seatbelt/longopts.js +377 -0
- package/dist/seatbelt/longopts.js.map +1 -0
- package/dist/seatbelt/mapper.d.ts +83 -0
- package/dist/seatbelt/mapper.d.ts.map +1 -0
- package/dist/seatbelt/mapper.js +10581 -0
- package/dist/seatbelt/mapper.js.map +1 -0
- package/dist/seatbelt/record.d.ts +94 -0
- package/dist/seatbelt/record.d.ts.map +1 -0
- package/dist/seatbelt/record.js +776 -0
- package/dist/seatbelt/record.js.map +1 -0
- package/dist/seatbelt/tables.d.ts +82 -0
- package/dist/seatbelt/tables.d.ts.map +1 -0
- package/dist/seatbelt/tables.js +352 -0
- package/dist/seatbelt/tables.js.map +1 -0
- package/dist/seatbelt-deploy.d.ts +100 -0
- package/dist/seatbelt-deploy.d.ts.map +1 -0
- package/dist/seatbelt-deploy.js +393 -0
- package/dist/seatbelt-deploy.js.map +1 -0
- package/dist/seatbelt-fs.d.ts +30 -0
- package/dist/seatbelt-fs.d.ts.map +1 -0
- package/dist/seatbelt-fs.js +94 -0
- package/dist/seatbelt-fs.js.map +1 -0
- package/dist/seatbelt-git.d.ts +212 -0
- package/dist/seatbelt-git.d.ts.map +1 -0
- package/dist/seatbelt-git.js +844 -0
- package/dist/seatbelt-git.js.map +1 -0
- package/dist/seatbelt-mode.d.ts +77 -0
- package/dist/seatbelt-mode.d.ts.map +1 -0
- package/dist/seatbelt-mode.js +496 -0
- package/dist/seatbelt-mode.js.map +1 -0
- package/dist/seatbelt-path.d.ts +123 -0
- package/dist/seatbelt-path.d.ts.map +1 -0
- package/dist/seatbelt-path.js +482 -0
- package/dist/seatbelt-path.js.map +1 -0
- package/dist/seatbelt-scripts.d.ts +109 -0
- package/dist/seatbelt-scripts.d.ts.map +1 -0
- package/dist/seatbelt-scripts.js +1060 -0
- package/dist/seatbelt-scripts.js.map +1 -0
- package/dist/secret-paths.d.ts +17 -0
- package/dist/secret-paths.d.ts.map +1 -0
- package/dist/secret-paths.js +32 -0
- package/dist/secret-paths.js.map +1 -0
- package/dist/session-loop-guard.d.ts.map +1 -1
- package/dist/session-loop-guard.js +15 -5
- package/dist/session-loop-guard.js.map +1 -1
- package/dist/tool-catalog.js +2 -2
- package/dist/tool-catalog.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/server.json +3 -3
- package/vendor/classifier/MANIFEST.json +25 -0
- package/vendor/classifier/classifier.js +1356 -0
- package/vendor/classifier/policy-baseline.js +187 -0
- package/vendor/classifier/redact.js +59 -0
- package/vendor/classifier/shell-parse.js +455 -0
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PUBLISH_LIFECYCLE = exports.MAX_MAKE_EXPANDED_BYTES = exports.MAX_MAKE_COMMANDS = exports.MAX_MAKE_RECIPE_LINES = exports.MAX_MAKE_RULES = exports.MAX_MAKE_PREREQUISITES = exports.MAX_MAKE_TARGETS_PER_RULE = exports.MAX_MAKE_DEPTH = exports.MAX_MANIFEST_BYTES = exports.MAX_SCRIPT_BYTES = void 0;
|
|
4
|
+
exports.resolvePackageScript = resolvePackageScript;
|
|
5
|
+
exports.packageCommands = packageCommands;
|
|
6
|
+
exports.resolveMakeTargets = resolveMakeTargets;
|
|
7
|
+
exports.readScriptFile = readScriptFile;
|
|
8
|
+
const seatbelt_path_1 = require("./seatbelt-path");
|
|
9
|
+
const seatbelt_git_1 = require("./seatbelt-git");
|
|
10
|
+
// Indirect command resolution for S2/S3 (spec §1.2, abuse rows A11/A12):
|
|
11
|
+
// package.json scripts, Makefile recipes and script files are read at hook
|
|
12
|
+
// time so a harmless-looking name cannot hide a deploy or publish. Anything
|
|
13
|
+
// that cannot be read exactly comes back 'unknown'.
|
|
14
|
+
exports.MAX_SCRIPT_BYTES = 64 * 1024;
|
|
15
|
+
exports.MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
16
|
+
exports.MAX_MAKE_DEPTH = 3;
|
|
17
|
+
/** Parse budgets: over any of them the Makefile is 'unknown' (time and memory stay linear in its size). */
|
|
18
|
+
exports.MAX_MAKE_TARGETS_PER_RULE = 256;
|
|
19
|
+
exports.MAX_MAKE_PREREQUISITES = 4096;
|
|
20
|
+
exports.MAX_MAKE_RULES = 4096;
|
|
21
|
+
exports.MAX_MAKE_RECIPE_LINES = 16384;
|
|
22
|
+
exports.MAX_MAKE_COMMANDS = 1024;
|
|
23
|
+
exports.MAX_MAKE_EXPANDED_BYTES = 256 * 1024;
|
|
24
|
+
function readResolved(fs, path, maxBytes) {
|
|
25
|
+
const real = (0, seatbelt_path_1.realPath)(fs, path, { followFinal: true });
|
|
26
|
+
if (real.status !== 'resolved')
|
|
27
|
+
return { status: 'unknown', reason: 'unreadable' };
|
|
28
|
+
if (!real.exists)
|
|
29
|
+
return { status: 'absent' };
|
|
30
|
+
try {
|
|
31
|
+
const entry = fs.lstat(real.path);
|
|
32
|
+
if (!entry)
|
|
33
|
+
return { status: 'absent' };
|
|
34
|
+
if (entry.type !== 'file')
|
|
35
|
+
return { status: 'unknown', reason: 'not_file' };
|
|
36
|
+
if (entry.size > maxBytes)
|
|
37
|
+
return { status: 'unknown', reason: 'too_large' };
|
|
38
|
+
const text = fs.readFile(real.path, maxBytes);
|
|
39
|
+
return text === null ? { status: 'unknown', reason: 'too_large' } : { status: 'ok', text, path: real.path };
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return { status: 'unknown', reason: 'unreadable' };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function parentOf(dir) {
|
|
46
|
+
if (dir === '/')
|
|
47
|
+
return null;
|
|
48
|
+
return dir.slice(0, dir.lastIndexOf('/')) || '/';
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The commands `npm|pnpm|yarn|bun run <name>` executes: pre<name>, <name>,
|
|
52
|
+
* post<name>, from the nearest package.json at or above dir (npm's prefix).
|
|
53
|
+
* A missing script is 'absent' (the runner fails); a malformed manifest is unknown.
|
|
54
|
+
*/
|
|
55
|
+
function resolvePackageScript(fs, dir, name) {
|
|
56
|
+
return packageCommands(fs, dir, [`pre${name}`, name, `post${name}`]);
|
|
57
|
+
}
|
|
58
|
+
/** Lifecycle scripts `npm publish` runs (S3 "lifecycle prepublish*"). */
|
|
59
|
+
exports.PUBLISH_LIFECYCLE = ['prepublish', 'prepare', 'prepublishOnly', 'prepack', 'postpack', 'publish', 'postpublish'];
|
|
60
|
+
function packageCommands(fs, dir, names) {
|
|
61
|
+
let current = dir;
|
|
62
|
+
while (current) {
|
|
63
|
+
const read = readResolved(fs, current === '/' ? '/package.json' : `${current}/package.json`, exports.MAX_MANIFEST_BYTES);
|
|
64
|
+
if (read.status === 'unknown')
|
|
65
|
+
return { status: 'unknown', reason: read.reason };
|
|
66
|
+
if (read.status === 'ok') {
|
|
67
|
+
let manifest;
|
|
68
|
+
try {
|
|
69
|
+
manifest = JSON.parse(read.text);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return { status: 'unknown', reason: 'malformed' };
|
|
73
|
+
}
|
|
74
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
75
|
+
return { status: 'unknown', reason: 'malformed' };
|
|
76
|
+
const scripts = manifest.scripts;
|
|
77
|
+
if (scripts !== undefined && (typeof scripts !== 'object' || scripts === null || Array.isArray(scripts)))
|
|
78
|
+
return { status: 'unknown', reason: 'malformed' };
|
|
79
|
+
const table = (scripts || {});
|
|
80
|
+
const commands = [];
|
|
81
|
+
for (const name of names) {
|
|
82
|
+
if (!Object.prototype.hasOwnProperty.call(table, name))
|
|
83
|
+
continue;
|
|
84
|
+
const command = table[name];
|
|
85
|
+
if (typeof command !== 'string')
|
|
86
|
+
return { status: 'unknown', reason: 'malformed' };
|
|
87
|
+
commands.push({ name, command });
|
|
88
|
+
}
|
|
89
|
+
return { status: 'ok', packageDir: current, commands };
|
|
90
|
+
}
|
|
91
|
+
current = parentOf(current);
|
|
92
|
+
}
|
|
93
|
+
return { status: 'absent' };
|
|
94
|
+
}
|
|
95
|
+
const MAKEFILE_NAMES = ['GNUmakefile', 'makefile', 'Makefile'];
|
|
96
|
+
/**
|
|
97
|
+
* Special targets that only tune make's behaviour; every other dot-target
|
|
98
|
+
* (suffix rules, .DEFAULT, .SECONDEXPANSION, .ONESHELL, .EXPORT_ALL_VARIABLES
|
|
99
|
+
* ...) can change which recipe runs or what it runs with.
|
|
100
|
+
*/
|
|
101
|
+
const MAKE_HARMLESS_SPECIAL_TARGETS = new Set(['.PHONY', '.PRECIOUS', '.INTERMEDIATE', '.SECONDARY', '.SILENT', '.IGNORE', '.DELETE_ON_ERROR', '.LOW_RESOLUTION_TIME', '.NOTPARALLEL', '.POSIX']);
|
|
102
|
+
/** Variables that change which program runs the recipes or which files make reads: assigning one is dynamic. */
|
|
103
|
+
const MAKE_EXECUTION_VARIABLES = new Set(['SHELL', 'MAKE', 'MAKEFLAGS', 'MFLAGS', 'GNUMAKEFLAGS', 'MAKEFILES', 'MAKEOVERRIDES', 'MAKELEVEL', 'MAKE_COMMAND', 'MAKECMDGOALS', 'VPATH', 'CURDIR']);
|
|
104
|
+
/** Environment that changes how make itself reads files or resolves variables: any of them present makes the answer dynamic. */
|
|
105
|
+
const MAKE_RELOCATING_ENV = ['MAKEFLAGS', 'GNUMAKEFLAGS', 'MFLAGS', 'MAKEFILES', 'MAKEOVERRIDES'];
|
|
106
|
+
/** A plain system shell path is the one SHELL value that is understood. */
|
|
107
|
+
const MAKE_SAFE_SHELL = /^(?:\/usr)?(?:\/local)?\/bin\/(?:sh|bash|zsh|dash|ksh|ash)$/;
|
|
108
|
+
/**
|
|
109
|
+
* Names that change what a recipe's program is or where it runs: shell start-up
|
|
110
|
+
* files, search paths, loaders, relocated tool state. make hands every variable
|
|
111
|
+
* that came from the environment or the command line to each recipe (and all of
|
|
112
|
+
* them under `.EXPORT_ALL_VARIABLES` or `export`), so an assignment to one of
|
|
113
|
+
* these, in the file or on the command line, is dynamic whether or not this
|
|
114
|
+
* resolver can tell that make exports it. Anything else is only a recipe
|
|
115
|
+
* environment change when the name is in the caller's environment.
|
|
116
|
+
*/
|
|
117
|
+
const MAKE_RECIPE_ENV_DENY = new RegExp('^(?:' +
|
|
118
|
+
[
|
|
119
|
+
'PATH', 'BASH_ENV', 'ENV', 'SHELL', 'SHELLOPTS', 'BASHOPTS', 'IFS', 'CDPATH', 'ZDOTDIR', 'HOME', 'TMPDIR', 'PS4', 'PROMPT_COMMAND',
|
|
120
|
+
'BASH_.*', 'GIT_.*', 'LD_.*', 'DYLD_.*', 'NODE_.*', 'PYTHON.*', 'PERL.*', 'RUBY.*', 'XDG_.*',
|
|
121
|
+
'EDITOR', 'VISUAL', 'PAGER', 'BROWSER', 'LESSOPEN', 'LOCPATH', 'NLSPATH', 'GCONV_PATH', 'HOSTALIASES',
|
|
122
|
+
'JAVA_TOOL_OPTIONS', '_JAVA_OPTIONS', 'JDK_JAVA_OPTIONS', 'CLASSPATH', 'GOFLAGS', 'RUSTC_WRAPPER', 'RUSTFLAGS',
|
|
123
|
+
].join('|') +
|
|
124
|
+
')$');
|
|
125
|
+
/** npm reads its configuration from `npm_config_*` in either case. */
|
|
126
|
+
const MAKE_RECIPE_ENV_DENY_ANY_CASE = /^(?:npm_config_.*|path)$/i;
|
|
127
|
+
/**
|
|
128
|
+
* Names that make every recipe (or the git, node or tool it starts) run other
|
|
129
|
+
* code or somewhere else: shell start-up files, the dynamic loader, relocated
|
|
130
|
+
* git. A routine environment has none of them, so one present in the caller's
|
|
131
|
+
* environment (the hook's, or a prefix assignment of the command) is dynamic.
|
|
132
|
+
*/
|
|
133
|
+
const MAKE_INJECTION_ENV = new RegExp(`^(?:BASH_ENV|ENV|SHELLOPTS|BASHOPTS|PS4|PROMPT_COMMAND|IFS|CDPATH|ZDOTDIR|BASH_FUNC_.*|LD_.*|DYLD_.*|${seatbelt_git_1.GIT_RELOCATING_ENV.join('|')})$`);
|
|
134
|
+
/**
|
|
135
|
+
* Variables GNU make defines before it reads any file (`make -p -f /dev/null`
|
|
136
|
+
* 4.3, plus the names of later versions). `CC ?= gcc` does not assign (CC is
|
|
137
|
+
* `cc`) and `RM += -r` is `rm -f -r`: with no assignment from the file their
|
|
138
|
+
* value is not known here, so `?=` and `+=` on them are unresolved.
|
|
139
|
+
*/
|
|
140
|
+
const MAKE_BUILTIN_VARIABLES = new Set([
|
|
141
|
+
'AR', 'ARFLAGS', 'AS', 'CC', 'CO', 'COFLAGS', 'CPP', 'CTANGLE', 'CWEAVE', 'CXX', 'F77', 'F77FLAGS', 'FC', 'GET', 'LD', 'LEX', 'LINT', 'M2C', 'MAKEINFO', 'OBJC', 'OUTPUT_OPTION', 'PC', 'RM', 'TANGLE', 'TEX', 'TEXI2DVI', 'WEAVE', 'YACC',
|
|
142
|
+
'MAKE_HOST', 'MAKE_VERSION', 'MAKE_TERMOUT', 'MAKE_TERMERR', 'MAKEFILE_LIST', 'SUFFIXES', 'GPATH',
|
|
143
|
+
]);
|
|
144
|
+
const MAKE_BUILTIN_VARIABLE_FAMILIES = /^(?:COMPILE|LINK|PREPROCESS|YACC|LEX|LINT)\./;
|
|
145
|
+
/** Make functions with no side effect (not evaluated here, but a use of their result is unresolved, not an execution at read time). */
|
|
146
|
+
const MAKE_PURE_FUNCTIONS = new Set(['subst', 'patsubst', 'strip', 'findstring', 'filter', 'filter-out', 'sort', 'word', 'wordlist', 'words', 'firstword', 'lastword', 'dir', 'notdir', 'suffix', 'basename', 'addsuffix', 'addprefix', 'join', 'wildcard', 'realpath', 'abspath']);
|
|
147
|
+
const MAKE_SIMPLE_FLAGS = new Set(['-s', '-k', '-w', '--silent', '--quiet', '--keep-going', '--no-print-directory', '--print-directory']);
|
|
148
|
+
/** The very function or object Object.prototype hands every object under this name (`toString`, `constructor`, `__proto__` ...): not an environment variable. */
|
|
149
|
+
function inheritedBuiltin(env, name, value) {
|
|
150
|
+
if (Object.prototype.hasOwnProperty.call(env, name))
|
|
151
|
+
return false;
|
|
152
|
+
if (name === '__proto__')
|
|
153
|
+
return value === Object.getPrototypeOf(env);
|
|
154
|
+
return name in Object.prototype && value === Object.prototype[name];
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* A simple Makefile recipe parse: explicit rules, recipe lines, prerequisites
|
|
158
|
+
* (depth ≤3) and variable references. Expansion follows GNU make: `=` is
|
|
159
|
+
* deferred, `:=`/`::=` are expanded when read, `?=` and `+=` need the prior
|
|
160
|
+
* value (the environment, when `options.env` is not given, is unknown), target
|
|
161
|
+
* names and prerequisites are expanded when the rule line is read and split
|
|
162
|
+
* into words, recipes after the whole file is read. `$(MAKE) <targets>` in a
|
|
163
|
+
* recipe is followed in the same file. `include`, conditionals, pattern or
|
|
164
|
+
* suffix rules, functions (`$(shell …)`), target-specific variables, variables
|
|
165
|
+
* that change the shell, or an unknown variable make the answer dynamic, which
|
|
166
|
+
* the mapper treats like an unknown script.
|
|
167
|
+
*
|
|
168
|
+
* `options.env` is the environment make would run in (the hook's, plus the
|
|
169
|
+
* command's prefix assignments). A caller MUST pass it: with it omitted every
|
|
170
|
+
* answer is dynamic, because MAKEFLAGS/MAKEFILES, `?=` and the names make
|
|
171
|
+
* exports to recipes all depend on it.
|
|
172
|
+
*
|
|
173
|
+
* Allowlist: an answer is definite only when none of these holds, each of which
|
|
174
|
+
* makes make run something the file does not say, or run it differently:
|
|
175
|
+
* `options.env` omitted, holding a non-string (an own property, enumerable or
|
|
176
|
+
* not), MAKEFLAGS-like names, shell
|
|
177
|
+
* start-up / loader / git-relocating names, or a PATH entry that is relative,
|
|
178
|
+
* non-canonical or inside the makefile's directory; an assignment (file or
|
|
179
|
+
* command line) to a dot-name, an execution variable, a name make hands to
|
|
180
|
+
* recipes (MAKE_RECIPE_ENV_DENY) or a name in the environment; `export`,
|
|
181
|
+
* `unexport`, `.EXPORT_ALL_VARIABLES`; `?=`/`+=` on a variable make defines
|
|
182
|
+
* itself; a command-line value with `$` or a control character, or a
|
|
183
|
+
* command-line name no expansion reaches; an assignment the command line overrides
|
|
184
|
+
* (make still reads it) that is `!=` or has a right side not proven inert; an assignment to a name the file assigned with
|
|
185
|
+
* `override` earlier (make keeps the override and still reads the right side); a recipe-less target that is not
|
|
186
|
+
* .PHONY and could have a source file (built-in rules); a rule for the makefile
|
|
187
|
+
* itself; two recipes for one target. A Makefile with a NUL byte, or a CR that
|
|
188
|
+
* does not end a CRLF, is `unknown` (control_character). The mapper must re-classify `commands`.
|
|
189
|
+
*/
|
|
190
|
+
function resolveMakeTargets(fs, dir, targets, options = {}) {
|
|
191
|
+
let read = { status: 'absent' };
|
|
192
|
+
if (options.makefile)
|
|
193
|
+
read = readResolved(fs, options.makefile.startsWith('/') ? options.makefile : `${dir}/${options.makefile}`, exports.MAX_MANIFEST_BYTES);
|
|
194
|
+
else {
|
|
195
|
+
for (const name of MAKEFILE_NAMES) {
|
|
196
|
+
read = readResolved(fs, `${dir}/${name}`, exports.MAX_MANIFEST_BYTES);
|
|
197
|
+
if (read.status !== 'absent')
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (read.status !== 'ok')
|
|
202
|
+
return read.status === 'absent' ? read : { status: 'unknown', reason: read.reason };
|
|
203
|
+
// make drops the rest of a line after a NUL, and the newline with it: the next line is joined to this one, so a NUL
|
|
204
|
+
// can hide a rule header or a recipe line from a reader that keeps the text. A CR that does not end a CRLF is not a
|
|
205
|
+
// line end to make, though it is one to other readers. Neither is modelled: the file is not read.
|
|
206
|
+
if (read.text.includes('\0') || /\r(?!\n)/.test(read.text))
|
|
207
|
+
return { status: 'unknown', reason: 'control_character' };
|
|
208
|
+
const variables = new Map();
|
|
209
|
+
const rules = new Map();
|
|
210
|
+
const makefileNames = new Set([...MAKEFILE_NAMES, read.path.slice(read.path.lastIndexOf('/') + 1)]);
|
|
211
|
+
let firstTarget = null;
|
|
212
|
+
const phony = new Set();
|
|
213
|
+
let dynamic = false;
|
|
214
|
+
let current = null;
|
|
215
|
+
let prerequisiteCount = 0;
|
|
216
|
+
let recipeCount = 0;
|
|
217
|
+
const overBudget = { status: 'unknown', reason: 'too_complex' };
|
|
218
|
+
// The environment make would see. Unknown (undefined) means a name the file
|
|
219
|
+
// does not assign may still have a value, ?= / += cannot be resolved, and
|
|
220
|
+
// MAKEFLAGS / MAKEFILES / exported names cannot be ruled out: the answer is
|
|
221
|
+
// dynamic. A caller that states no environment never gets a definite answer.
|
|
222
|
+
const env = options.env;
|
|
223
|
+
/**
|
|
224
|
+
* A string value only. `constructor`, `__proto__` and `toString` (functions and objects Object.prototype hands every
|
|
225
|
+
* object) are not environment variables. Any other value that is neither undefined nor a string is not understood
|
|
226
|
+
* (make is handed it as some text): the answer is dynamic, whichever way the value was reached.
|
|
227
|
+
*/
|
|
228
|
+
const envValue = (name) => {
|
|
229
|
+
if (!env)
|
|
230
|
+
return undefined;
|
|
231
|
+
const value = env[name];
|
|
232
|
+
if (typeof value === 'string')
|
|
233
|
+
return value;
|
|
234
|
+
if (value !== undefined && !inheritedBuiltin(env, name, value))
|
|
235
|
+
dynamic = true;
|
|
236
|
+
return undefined;
|
|
237
|
+
};
|
|
238
|
+
if (!env || MAKE_RELOCATING_ENV.some((name) => envValue(name) !== undefined))
|
|
239
|
+
dynamic = true;
|
|
240
|
+
const makeDirectories = [dir, read.path.slice(0, read.path.lastIndexOf('/')) || '/'].map((directory) => directory.replace(/\/+$/, '') || '/');
|
|
241
|
+
const isTrustedPathEntry = (entry) => {
|
|
242
|
+
if (!/^(?:\/|[A-Za-z]:[\\/])/.test(entry) || /(?:^|[\\/])\.{1,2}(?:[\\/]|$)/.test(entry))
|
|
243
|
+
return false;
|
|
244
|
+
const bare = entry.replace(/\/+$/, '') || '/';
|
|
245
|
+
return !makeDirectories.some((directory) => directory === '/' || bare === directory || bare.startsWith(`${directory}/`));
|
|
246
|
+
};
|
|
247
|
+
const envNames = new Set();
|
|
248
|
+
if (env) {
|
|
249
|
+
// for...in: a name an object only inherits is still a name make would see; an own property that is not enumerable
|
|
250
|
+
// is one too (the same value `envValue` reads). A value that is not a string is not understood.
|
|
251
|
+
const names = new Set();
|
|
252
|
+
for (const name in env)
|
|
253
|
+
names.add(name);
|
|
254
|
+
for (const name of Object.getOwnPropertyNames(env))
|
|
255
|
+
names.add(name);
|
|
256
|
+
for (const name of names) {
|
|
257
|
+
const value = env[name];
|
|
258
|
+
if (typeof value !== 'string') {
|
|
259
|
+
if (value !== undefined)
|
|
260
|
+
dynamic = true;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
envNames.add(name.toUpperCase());
|
|
264
|
+
if (MAKE_INJECTION_ENV.test(name.toUpperCase()))
|
|
265
|
+
dynamic = true;
|
|
266
|
+
// A PATH entry that is relative, empty, not canonical or inside the directory make runs in makes `git` or
|
|
267
|
+
// `make` in a recipe a file of the project (the workspace root above it is the mapper's to model).
|
|
268
|
+
if (name.toUpperCase() === 'PATH' && value.split(value.includes(';') ? ';' : ':').some((entry) => !isTrustedPathEntry(entry)))
|
|
269
|
+
dynamic = true;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* make hands this name to every recipe, or it changes what runs or where:
|
|
274
|
+
* assigning it (in the file or on the command line, any operator) is dynamic.
|
|
275
|
+
* Dot-names are make's own settings; a name in the environment is exported
|
|
276
|
+
* with its new value; the rest is MAKE_RECIPE_ENV_DENY and MAKE_EXECUTION_VARIABLES.
|
|
277
|
+
*/
|
|
278
|
+
const changesWhatRuns = (name) => name.startsWith('.') || MAKE_EXECUTION_VARIABLES.has(name) || MAKE_RECIPE_ENV_DENY.test(name) || MAKE_RECIPE_ENV_DENY_ANY_CASE.test(name) || envNames.has(name.toUpperCase());
|
|
279
|
+
const isBuiltin = (name) => MAKE_BUILTIN_VARIABLES.has(name) || MAKE_BUILTIN_VARIABLE_FAMILIES.test(name);
|
|
280
|
+
const commandLine = new Map(Object.entries(options.overrides || {}));
|
|
281
|
+
// Names the file assigned with `override`: make keeps that value, and ignores every later assignment without `override`.
|
|
282
|
+
const overridden = new Set();
|
|
283
|
+
// A command-line variable is exported to every recipe and its value is expanded each time make builds a recipe
|
|
284
|
+
// environment, used or not: only a plain value and a name the Makefile itself uses (and that is not one of the lists) are definite.
|
|
285
|
+
const referenced = new Set();
|
|
286
|
+
for (const [name, value] of commandLine) {
|
|
287
|
+
const shell = name === 'SHELL' && MAKE_SAFE_SHELL.test(value);
|
|
288
|
+
if (!shell && changesWhatRuns(name))
|
|
289
|
+
dynamic = true;
|
|
290
|
+
if (/[$\x00-\x1f\x7f]/.test(value))
|
|
291
|
+
dynamic = true;
|
|
292
|
+
variables.set(name, { flavor: 'recursive', value, unresolved: false });
|
|
293
|
+
}
|
|
294
|
+
// Every expansion is charged to one budget, so nested variables cannot blow up.
|
|
295
|
+
let expandedBytes = 0;
|
|
296
|
+
let exhausted = false;
|
|
297
|
+
// Set while the right side of an assignment make drops is only looked at: no name is marked as used, and a value this
|
|
298
|
+
// resolver could not compute counts as a function with effects (it may hold one).
|
|
299
|
+
let probing = false;
|
|
300
|
+
const MAKE_AUTOMATIC = /^[@<^*?%+|]$/;
|
|
301
|
+
/**
|
|
302
|
+
* `state.bad` is set when a reference could not be resolved (the reference
|
|
303
|
+
* text is kept as written). `state.fn` is set when make would run a function
|
|
304
|
+
* with side effects (`$(shell …)`, `$(eval …)`, `$(file …)`, unparsed syntax):
|
|
305
|
+
* in a context make expands when it reads the line, that executes whether or
|
|
306
|
+
* not the target is wanted.
|
|
307
|
+
*/
|
|
308
|
+
const expand = (text, state, depth = 0) => {
|
|
309
|
+
if (!text.includes('$'))
|
|
310
|
+
return text;
|
|
311
|
+
expandedBytes += text.length;
|
|
312
|
+
if (expandedBytes > exports.MAX_MAKE_EXPANDED_BYTES) {
|
|
313
|
+
exhausted = true;
|
|
314
|
+
return '';
|
|
315
|
+
}
|
|
316
|
+
if (depth > 8) {
|
|
317
|
+
state.bad = true;
|
|
318
|
+
state.fn = true;
|
|
319
|
+
return text;
|
|
320
|
+
}
|
|
321
|
+
let out = '';
|
|
322
|
+
let i = 0;
|
|
323
|
+
while (i < text.length) {
|
|
324
|
+
const ch = text[i];
|
|
325
|
+
if (ch !== '$') {
|
|
326
|
+
out += ch;
|
|
327
|
+
i += 1;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const next = text[i + 1];
|
|
331
|
+
if (next === undefined) {
|
|
332
|
+
state.bad = true;
|
|
333
|
+
state.fn = true;
|
|
334
|
+
out += '$';
|
|
335
|
+
i += 1;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (next === '$') {
|
|
339
|
+
out += '$';
|
|
340
|
+
i += 2;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
let reference;
|
|
344
|
+
let name;
|
|
345
|
+
if (next === '(' || next === '{') {
|
|
346
|
+
const close = next === '(' ? ')' : '}';
|
|
347
|
+
let level = 1;
|
|
348
|
+
let j = i + 2;
|
|
349
|
+
while (j < text.length && level > 0) {
|
|
350
|
+
if (text[j] === next)
|
|
351
|
+
level += 1;
|
|
352
|
+
else if (text[j] === close)
|
|
353
|
+
level -= 1;
|
|
354
|
+
j += 1;
|
|
355
|
+
}
|
|
356
|
+
if (level !== 0) {
|
|
357
|
+
// Unbalanced: nothing after this point can be read reliably.
|
|
358
|
+
state.bad = true;
|
|
359
|
+
state.fn = true;
|
|
360
|
+
out += text.slice(i);
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
reference = text.slice(i, j);
|
|
364
|
+
const inner = text.slice(i + 2, j - 1);
|
|
365
|
+
name = inner.includes('$') ? expand(inner, state, depth + 1) : inner;
|
|
366
|
+
i = j;
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
reference = text.slice(i, i + 2);
|
|
370
|
+
name = next;
|
|
371
|
+
i += 2;
|
|
372
|
+
}
|
|
373
|
+
if (exhausted)
|
|
374
|
+
return '';
|
|
375
|
+
if (MAKE_AUTOMATIC.test(name)) {
|
|
376
|
+
out += reference;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
// A function call (`$(shell …)`, `$(subst …)`), a substitution reference
|
|
380
|
+
// or an automatic-variable modifier is not evaluated here.
|
|
381
|
+
if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(name)) {
|
|
382
|
+
state.bad = true;
|
|
383
|
+
const functionName = /^([A-Za-z][A-Za-z-]*)\s/.exec(name)?.[1];
|
|
384
|
+
if (functionName !== undefined) {
|
|
385
|
+
if (!MAKE_PURE_FUNCTIONS.has(functionName))
|
|
386
|
+
state.fn = true;
|
|
387
|
+
}
|
|
388
|
+
else if (!/^[A-Za-z_][A-Za-z0-9_.-]*:[^\s=]*=\S*$/.test(name))
|
|
389
|
+
state.fn = true;
|
|
390
|
+
out += reference;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (!probing)
|
|
394
|
+
referenced.add(name);
|
|
395
|
+
const variable = variables.get(name);
|
|
396
|
+
if (variable === undefined) {
|
|
397
|
+
const fromEnv = envValue(name);
|
|
398
|
+
if (fromEnv !== undefined) {
|
|
399
|
+
const inner = { bad: false, fn: false };
|
|
400
|
+
out += expand(fromEnv, inner, depth + 1);
|
|
401
|
+
if (inner.bad)
|
|
402
|
+
state.bad = true;
|
|
403
|
+
if (inner.fn)
|
|
404
|
+
state.fn = true;
|
|
405
|
+
}
|
|
406
|
+
else if (name === 'MAKE' && !options.overrides?.MAKE)
|
|
407
|
+
out += 'make';
|
|
408
|
+
else {
|
|
409
|
+
state.bad = true;
|
|
410
|
+
out += reference;
|
|
411
|
+
}
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (variable.unresolved) {
|
|
415
|
+
state.bad = true;
|
|
416
|
+
// A deferred value that holds a `$` may hold a call with effects (`CC += $(shell …)` on a variable make defines
|
|
417
|
+
// itself): wherever make expands it when it reads the line, it runs, used or not. A simple value is text make has
|
|
418
|
+
// already expanded (its calls ran when it was assigned and set `fn` then). While a dropped right side is only
|
|
419
|
+
// looked at, any unresolved value counts.
|
|
420
|
+
if (probing || (variable.flavor === 'recursive' && variable.value.includes('$')))
|
|
421
|
+
state.fn = true;
|
|
422
|
+
out += reference;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (variable.flavor === 'simple')
|
|
426
|
+
out += variable.value;
|
|
427
|
+
else {
|
|
428
|
+
const inner = { bad: false, fn: false };
|
|
429
|
+
out += expand(variable.value, inner, depth + 1);
|
|
430
|
+
if (inner.bad)
|
|
431
|
+
state.bad = true;
|
|
432
|
+
if (inner.fn)
|
|
433
|
+
state.fn = true;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return out;
|
|
437
|
+
};
|
|
438
|
+
const words = (value) => value.split(/\s+/).filter(Boolean);
|
|
439
|
+
const lines = read.text.replace(/\\\r?\n/g, ' ').split(/\r?\n/);
|
|
440
|
+
for (const rawLine of lines) {
|
|
441
|
+
let line = rawLine;
|
|
442
|
+
if (line.startsWith('\t')) {
|
|
443
|
+
if (current) {
|
|
444
|
+
recipeCount += 1;
|
|
445
|
+
if (recipeCount > exports.MAX_MAKE_RECIPE_LINES)
|
|
446
|
+
return overBudget;
|
|
447
|
+
current.recipe.push(line.slice(1));
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
// Outside a rule a tab-led line is ordinary makefile syntax (GNU make read.c).
|
|
451
|
+
line = line.replace(/^\t+/, '');
|
|
452
|
+
}
|
|
453
|
+
// `\#` is a literal hash; this resolver does not model it.
|
|
454
|
+
if (line.includes('\\#'))
|
|
455
|
+
dynamic = true;
|
|
456
|
+
const trimmed = line.replace(/#.*$/, '').trim();
|
|
457
|
+
if (!trimmed)
|
|
458
|
+
continue;
|
|
459
|
+
if (/^(-?include|sinclude|ifeq|ifneq|ifdef|ifndef|else|endif|define|endef|export|unexport|undefine|vpath|load|-load)\b/.test(trimmed)) {
|
|
460
|
+
dynamic = true;
|
|
461
|
+
current = null;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const assign = /^(override\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*(\?=|:::=|::=|:=|\+=|!=|=)\s*(.*)$/.exec(trimmed);
|
|
465
|
+
if (assign) {
|
|
466
|
+
const [, overrideKeyword, name, op, value] = assign;
|
|
467
|
+
current = null;
|
|
468
|
+
const safeShell = name === 'SHELL' && (op === '=' || op === ':=' || op === '::=') && MAKE_SAFE_SHELL.test(value.trim());
|
|
469
|
+
if (!safeShell && changesWhatRuns(name))
|
|
470
|
+
dynamic = true;
|
|
471
|
+
// A variable given on the command line wins over the file's assignments (unless `override`). make still reads the
|
|
472
|
+
// dropped assignment: `!=` runs its command, and `:=` / `::=` / `:::=` expand their right side, at read time and
|
|
473
|
+
// under `make -n`. The right side is looked at as if it were assigned (`=`, `?=` and `+=` too: make does not
|
|
474
|
+
// expand those, a call in them is still not proven inert): a call with effects, or a value not understood, is dynamic.
|
|
475
|
+
// A name the file assigned with `override` is kept the same way (the value stays, the right side is read as above), and
|
|
476
|
+
// the answer is not definite: what make keeps is decided by where a name came from, which the later line does not show.
|
|
477
|
+
const keptByCommandLine = commandLine.has(name);
|
|
478
|
+
if (!overrideKeyword && (keptByCommandLine || overridden.has(name))) {
|
|
479
|
+
if (op === '!=')
|
|
480
|
+
dynamic = true;
|
|
481
|
+
else {
|
|
482
|
+
const state = { bad: false, fn: false };
|
|
483
|
+
probing = true;
|
|
484
|
+
try {
|
|
485
|
+
expand(value, state);
|
|
486
|
+
}
|
|
487
|
+
finally {
|
|
488
|
+
probing = false;
|
|
489
|
+
}
|
|
490
|
+
if (exhausted)
|
|
491
|
+
return overBudget;
|
|
492
|
+
if (state.fn)
|
|
493
|
+
dynamic = true;
|
|
494
|
+
}
|
|
495
|
+
if (!keptByCommandLine)
|
|
496
|
+
dynamic = true;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
const existing = variables.get(name);
|
|
500
|
+
if (op === '!=') {
|
|
501
|
+
dynamic = true;
|
|
502
|
+
variables.set(name, { flavor: 'simple', value: '', unresolved: true });
|
|
503
|
+
}
|
|
504
|
+
else if (op === '=')
|
|
505
|
+
variables.set(name, { flavor: 'recursive', value, unresolved: false });
|
|
506
|
+
else if (op === ':=' || op === '::=') {
|
|
507
|
+
const state = { bad: false, fn: false };
|
|
508
|
+
variables.set(name, { flavor: 'simple', value: expand(value, state), unresolved: state.bad });
|
|
509
|
+
if (state.fn)
|
|
510
|
+
dynamic = true;
|
|
511
|
+
}
|
|
512
|
+
else if (op === ':::=') {
|
|
513
|
+
const state = { bad: false, fn: false };
|
|
514
|
+
variables.set(name, { flavor: 'recursive', value: expand(value, state).replace(/\$/g, '$$$$'), unresolved: state.bad });
|
|
515
|
+
if (state.fn)
|
|
516
|
+
dynamic = true;
|
|
517
|
+
}
|
|
518
|
+
else if (op === '?=') {
|
|
519
|
+
// Defined already (in the file or on the command line): unchanged. Otherwise the environment decides.
|
|
520
|
+
if (existing === undefined) {
|
|
521
|
+
const fromEnv = envValue(name);
|
|
522
|
+
if (fromEnv !== undefined)
|
|
523
|
+
variables.set(name, { flavor: 'recursive', value: fromEnv, unresolved: false });
|
|
524
|
+
else if (env && !isBuiltin(name))
|
|
525
|
+
variables.set(name, { flavor: 'recursive', value, unresolved: false });
|
|
526
|
+
else
|
|
527
|
+
variables.set(name, { flavor: 'recursive', value, unresolved: true });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
else if (op === '+=') {
|
|
531
|
+
if (existing === undefined) {
|
|
532
|
+
const fromEnv = envValue(name);
|
|
533
|
+
if (fromEnv !== undefined)
|
|
534
|
+
variables.set(name, { flavor: 'recursive', value: `${fromEnv} ${value}`, unresolved: false });
|
|
535
|
+
else if (env && !isBuiltin(name))
|
|
536
|
+
variables.set(name, { flavor: 'recursive', value, unresolved: false });
|
|
537
|
+
else
|
|
538
|
+
variables.set(name, { flavor: 'recursive', value, unresolved: true });
|
|
539
|
+
}
|
|
540
|
+
else if (existing.flavor === 'simple') {
|
|
541
|
+
const state = { bad: false, fn: false };
|
|
542
|
+
const added = expand(value, state);
|
|
543
|
+
variables.set(name, { flavor: 'simple', value: `${existing.value} ${added}`.trim(), unresolved: existing.unresolved || state.bad });
|
|
544
|
+
if (state.fn)
|
|
545
|
+
dynamic = true;
|
|
546
|
+
}
|
|
547
|
+
else
|
|
548
|
+
variables.set(name, { flavor: 'recursive', value: `${existing.value} ${value}`.trim(), unresolved: existing.unresolved });
|
|
549
|
+
}
|
|
550
|
+
// `?=` on a name that is already defined assigns nothing: it does not make the name an overridden one.
|
|
551
|
+
if (overrideKeyword && !(op === '?=' && existing !== undefined))
|
|
552
|
+
overridden.add(name);
|
|
553
|
+
const stored = variables.get(name);
|
|
554
|
+
if (stored !== undefined && stored.value.length > exports.MAX_MAKE_EXPANDED_BYTES)
|
|
555
|
+
return overBudget;
|
|
556
|
+
if (exhausted)
|
|
557
|
+
return overBudget;
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
const rule = /^([^:=]+)::?([^=]*)$/.exec(trimmed);
|
|
561
|
+
if (!rule) {
|
|
562
|
+
// Target-specific variables (`t: V = x`), `.RECIPEPREFIX`, anything else this parse does not model.
|
|
563
|
+
dynamic = true;
|
|
564
|
+
current = null;
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
let prereqs = rule[2].trim();
|
|
568
|
+
let inlineRecipe = null;
|
|
569
|
+
const semi = prereqs.indexOf(';');
|
|
570
|
+
if (semi >= 0) {
|
|
571
|
+
inlineRecipe = prereqs.slice(semi + 1).trim();
|
|
572
|
+
prereqs = prereqs.slice(0, semi);
|
|
573
|
+
}
|
|
574
|
+
const nameState = { bad: false, fn: false };
|
|
575
|
+
const names = words(expand(rule[1].trim(), nameState));
|
|
576
|
+
if (exhausted)
|
|
577
|
+
return overBudget;
|
|
578
|
+
// A name that cannot be expanded may be the wanted target.
|
|
579
|
+
if (nameState.bad)
|
|
580
|
+
dynamic = true;
|
|
581
|
+
if (names.length > exports.MAX_MAKE_TARGETS_PER_RULE)
|
|
582
|
+
return overBudget;
|
|
583
|
+
const prerequisiteState = { bad: false, fn: false };
|
|
584
|
+
const expandedPrerequisites = expand(prereqs, prerequisiteState);
|
|
585
|
+
if (exhausted)
|
|
586
|
+
return overBudget;
|
|
587
|
+
// Make expands prerequisites when it reads the line: a function with effects runs for every rule, wanted or not.
|
|
588
|
+
if (prerequisiteState.fn)
|
|
589
|
+
dynamic = true;
|
|
590
|
+
const prerequisiteWords = words(expandedPrerequisites).filter((word) => word !== '|');
|
|
591
|
+
// `$` left over (automatic variables, `$$`), a second `:` (static pattern rule) or a wildcard cannot be followed.
|
|
592
|
+
const unresolved = prerequisiteState.bad || /[$:%*?[]/.test(expandedPrerequisites);
|
|
593
|
+
const ruleLine = { prerequisites: prerequisiteWords, unresolved, recipe: inlineRecipe ? [inlineRecipe] : [] };
|
|
594
|
+
prerequisiteCount += ruleLine.prerequisites.length;
|
|
595
|
+
if (prerequisiteCount > exports.MAX_MAKE_PREREQUISITES)
|
|
596
|
+
return overBudget;
|
|
597
|
+
current = ruleLine;
|
|
598
|
+
for (const name of names) {
|
|
599
|
+
// make brings the makefile itself up to date first: a rule for it runs whatever target is wanted.
|
|
600
|
+
if (makefileNames.has(name.replace(/^(?:\.\/)+/, '')))
|
|
601
|
+
dynamic = true;
|
|
602
|
+
if (name.includes('%'))
|
|
603
|
+
dynamic = true;
|
|
604
|
+
if (name.startsWith('.') && !MAKE_HARMLESS_SPECIAL_TARGETS.has(name))
|
|
605
|
+
dynamic = true;
|
|
606
|
+
if (name === '.PHONY')
|
|
607
|
+
for (const word of prerequisiteWords)
|
|
608
|
+
phony.add(word);
|
|
609
|
+
if (name.startsWith('.') && MAKE_HARMLESS_SPECIAL_TARGETS.has(name))
|
|
610
|
+
continue;
|
|
611
|
+
if (firstTarget === null && !name.includes('%') && !name.startsWith('.'))
|
|
612
|
+
firstTarget = name;
|
|
613
|
+
let entry = rules.get(name);
|
|
614
|
+
if (!entry) {
|
|
615
|
+
if (rules.size >= exports.MAX_MAKE_RULES)
|
|
616
|
+
return overBudget;
|
|
617
|
+
entry = { lines: [] };
|
|
618
|
+
rules.set(name, entry);
|
|
619
|
+
}
|
|
620
|
+
if (entry.lines[entry.lines.length - 1] !== ruleLine)
|
|
621
|
+
entry.lines.push(ruleLine);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
const commands = [];
|
|
625
|
+
const listings = new Map();
|
|
626
|
+
/**
|
|
627
|
+
* A target with no recipe of its own that is not .PHONY is searched for in
|
|
628
|
+
* make's built-in rules (`%.o: %.c`, `%: %.c`, `%: %.sh`, RCS/SCCS ...), which
|
|
629
|
+
* run `$(CC)`, `$(COMPILE.c)` ... on a source file. With a file in the
|
|
630
|
+
* directory that could be that source (or one that cannot be listed) the
|
|
631
|
+
* commands make runs are not in the file: dynamic.
|
|
632
|
+
*/
|
|
633
|
+
const implicitRuleMayApply = (target) => {
|
|
634
|
+
if (/[()$%*?[\]]/.test(target))
|
|
635
|
+
return true;
|
|
636
|
+
const slash = target.lastIndexOf('/');
|
|
637
|
+
const where = slash < 0 ? dir : target.startsWith('/') ? target.slice(0, slash) || '/' : `${dir}/${target.slice(0, slash)}`;
|
|
638
|
+
let entries = listings.get(where);
|
|
639
|
+
if (entries === undefined) {
|
|
640
|
+
try {
|
|
641
|
+
entries = fs.readdir(where);
|
|
642
|
+
}
|
|
643
|
+
catch {
|
|
644
|
+
entries = null;
|
|
645
|
+
}
|
|
646
|
+
listings.set(where, entries);
|
|
647
|
+
}
|
|
648
|
+
if (entries === null)
|
|
649
|
+
return true;
|
|
650
|
+
const base = target.slice(slash + 1);
|
|
651
|
+
const stem = base.split('.')[0];
|
|
652
|
+
return entries.some((entry) => entry === 'RCS' || entry === 'SCCS' || (entry !== base && (entry.startsWith(`${stem}.`) || entry === `${base},v` || entry === `s.${base}`)));
|
|
653
|
+
};
|
|
654
|
+
const visit = (target, depth, seen) => {
|
|
655
|
+
if (exhausted || seen.has(target))
|
|
656
|
+
return;
|
|
657
|
+
seen.add(target);
|
|
658
|
+
const rule = rules.get(target);
|
|
659
|
+
if (!rule?.lines.some((ruleLine) => ruleLine.recipe.length > 0) && !phony.has(target) && implicitRuleMayApply(target))
|
|
660
|
+
dynamic = true;
|
|
661
|
+
if (!rule)
|
|
662
|
+
return;
|
|
663
|
+
// Two rules with a recipe for one target: make keeps one (a warning) and drops the other, unless they are `::`
|
|
664
|
+
// rules; which commands run, and which variables they expand, is not read here.
|
|
665
|
+
if (rule.lines.filter((ruleLine) => ruleLine.recipe.length > 0).length > 1)
|
|
666
|
+
dynamic = true;
|
|
667
|
+
for (const ruleLine of rule.lines) {
|
|
668
|
+
if (ruleLine.unresolved)
|
|
669
|
+
dynamic = true;
|
|
670
|
+
if (depth < exports.MAX_MAKE_DEPTH)
|
|
671
|
+
for (const prerequisite of ruleLine.prerequisites)
|
|
672
|
+
visit(prerequisite, depth + 1, seen);
|
|
673
|
+
else if (ruleLine.prerequisites.length > 0)
|
|
674
|
+
dynamic = true;
|
|
675
|
+
}
|
|
676
|
+
for (const ruleLine of rule.lines) {
|
|
677
|
+
for (const line of ruleLine.recipe) {
|
|
678
|
+
if (commands.length >= exports.MAX_MAKE_COMMANDS) {
|
|
679
|
+
exhausted = true;
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const state = { bad: false, fn: false };
|
|
683
|
+
const command = expand(line, state).replace(/^[\s@+-]+/, '');
|
|
684
|
+
if (state.bad)
|
|
685
|
+
dynamic = true;
|
|
686
|
+
commands.push(command);
|
|
687
|
+
// `$(MAKE) …` or a command that starts with `make` (also through a variable: RUN = $(MAKE) x).
|
|
688
|
+
if (/\$[({]MAKE[)}]/.test(line) || /^g?make(\s|$)/.test(command)) {
|
|
689
|
+
// A sub-make reads the default makefile of its directory: -f is not passed down in MAKEFLAGS.
|
|
690
|
+
if (options.makefile)
|
|
691
|
+
dynamic = true;
|
|
692
|
+
else
|
|
693
|
+
followSubMake(command, depth, seen);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
// `$(MAKE) a b` runs the same file's targets again: follow plain forms; any
|
|
699
|
+
// other form (-C, -f, VAR=x, a chained command) cannot be followed.
|
|
700
|
+
const followSubMake = (command, depth, seen) => {
|
|
701
|
+
const tokens = /[;&|<>()`$"'\\\n]/.test(command) ? null : words(command);
|
|
702
|
+
if (!tokens || tokens.length === 0 || tokens[0] !== 'make') {
|
|
703
|
+
dynamic = true;
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const subTargets = [];
|
|
707
|
+
for (const token of tokens.slice(1)) {
|
|
708
|
+
if (MAKE_SIMPLE_FLAGS.has(token) || /^-j\d*$/.test(token))
|
|
709
|
+
continue;
|
|
710
|
+
if (/^[A-Za-z0-9_][A-Za-z0-9_./+-]*$/.test(token))
|
|
711
|
+
subTargets.push(token);
|
|
712
|
+
else {
|
|
713
|
+
dynamic = true;
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (depth + 1 > exports.MAX_MAKE_DEPTH) {
|
|
718
|
+
dynamic = true;
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const wantedHere = subTargets.length > 0 ? subTargets : firstTarget ? [firstTarget] : [];
|
|
722
|
+
if (wantedHere.length === 0)
|
|
723
|
+
dynamic = true;
|
|
724
|
+
for (const sub of wantedHere) {
|
|
725
|
+
if (!rules.has(sub))
|
|
726
|
+
dynamic = true;
|
|
727
|
+
// A fresh `seen`: a sub-make is a new run, though depth still bounds the recursion.
|
|
728
|
+
visit(sub, depth + 1, new Set(seen));
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
const wanted = targets.length > 0 ? targets : firstTarget ? [firstTarget] : [];
|
|
732
|
+
for (const target of wanted)
|
|
733
|
+
visit(target, 0, new Set());
|
|
734
|
+
if (exhausted)
|
|
735
|
+
return overBudget;
|
|
736
|
+
// A command-line variable the Makefile never mentions can only act through the environment of the recipes.
|
|
737
|
+
for (const [name, value] of commandLine)
|
|
738
|
+
if (!referenced.has(name) && !(name === 'SHELL' && MAKE_SAFE_SHELL.test(value)))
|
|
739
|
+
dynamic = true;
|
|
740
|
+
return { status: 'ok', makefile: read.path, commands, dynamic: dynamic || wanted.some((t) => !rules.has(t)) };
|
|
741
|
+
}
|
|
742
|
+
const SHELL_PROGRAMS = /^(?:sh|bash|zsh|dash|ksh|ash)$/;
|
|
743
|
+
/** Where a shebang's interpreter (or `env`) is trusted to be the program its name says; a copy elsewhere (a repo's `./bin/bash`) is unknown. */
|
|
744
|
+
const SYSTEM_BIN_DIRS = new Set(['/bin', '/usr/bin', '/usr/local/bin', '/sbin', '/usr/sbin', '/opt/homebrew/bin', '/opt/local/bin', '/usr/pkg/bin']);
|
|
745
|
+
const SHELL_FLAG_LETTERS = 'euxvfnCabhEBHPkT';
|
|
746
|
+
const SHELL_OPTION_NAMES = new Set(['pipefail', 'errexit', 'nounset', 'xtrace', 'noglob', 'noclobber', 'verbose', 'noexec', 'posix', 'allexport', 'errtrace', 'functrace']);
|
|
747
|
+
const SHELL_LONG_FLAGS = new Set(['--posix', '--noprofile', '--norc']);
|
|
748
|
+
const MAX_ENV_SPLIT_DEPTH = 4;
|
|
749
|
+
/**
|
|
750
|
+
* The only shebang lines a definite `shell` answer is given for: at most this
|
|
751
|
+
* many bytes (Linux before 5.1 cuts the line at 127; later kernels fail or cut at
|
|
752
|
+
* 255) of printable ASCII and tab. Anything else (CR, VT, FF, NUL, DEL, other
|
|
753
|
+
* control or non-ASCII characters) is split differently by the kernel, env -S and
|
|
754
|
+
* this parser, so it is never `shell`.
|
|
755
|
+
*/
|
|
756
|
+
const MAX_SHEBANG_LINE = 127;
|
|
757
|
+
const PLAIN_SHEBANG_LINE = /^[\x20-\x7e\t]*$/;
|
|
758
|
+
/** A control character (anything below space except tab, DEL, NUL ...). */
|
|
759
|
+
const CONTROL_CHARACTER = /[\x00-\x08\x0a-\x1f\x7f]/;
|
|
760
|
+
/** A line that is not on the allowlist is never a definite shell; an answer that was already `other` or `unknown` is kept (never looser). */
|
|
761
|
+
function degrade(result) {
|
|
762
|
+
return result.kind === 'shell' ? { kind: 'other' } : result;
|
|
763
|
+
}
|
|
764
|
+
/** coreutils `env -S` split-string: quotes, backslash escapes, `#` comment. Null for anything else (`$` expansion, `\c`, unknown escapes, unclosed quote). */
|
|
765
|
+
function splitEnvString(value) {
|
|
766
|
+
const escapes = { n: '\n', t: '\t', r: '\r', f: '\f', v: '\v', '\\': '\\', '"': '"', "'": "'", '#': '#', $: '$' };
|
|
767
|
+
const tokens = [];
|
|
768
|
+
let word = '';
|
|
769
|
+
let inWord = false;
|
|
770
|
+
let quote = null;
|
|
771
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
772
|
+
const ch = value[i];
|
|
773
|
+
if (ch === '$')
|
|
774
|
+
return null;
|
|
775
|
+
if (quote === "'") {
|
|
776
|
+
if (ch === "'")
|
|
777
|
+
quote = null;
|
|
778
|
+
else if (ch === '\\') {
|
|
779
|
+
const next = value[i + 1];
|
|
780
|
+
if (next !== '\\' && next !== "'")
|
|
781
|
+
return null;
|
|
782
|
+
word += next;
|
|
783
|
+
i += 1;
|
|
784
|
+
}
|
|
785
|
+
else
|
|
786
|
+
word += ch;
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
if (quote === '"') {
|
|
790
|
+
if (ch === '"')
|
|
791
|
+
quote = null;
|
|
792
|
+
else if (ch === '\\') {
|
|
793
|
+
const next = value[i + 1];
|
|
794
|
+
if (next === '_')
|
|
795
|
+
word += ' ';
|
|
796
|
+
else if (next !== undefined && Object.prototype.hasOwnProperty.call(escapes, next))
|
|
797
|
+
word += escapes[next];
|
|
798
|
+
else
|
|
799
|
+
return null;
|
|
800
|
+
i += 1;
|
|
801
|
+
}
|
|
802
|
+
else
|
|
803
|
+
word += ch;
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === '\f' || ch === '\v') {
|
|
807
|
+
if (inWord)
|
|
808
|
+
tokens.push(word);
|
|
809
|
+
word = '';
|
|
810
|
+
inWord = false;
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
if (ch === '#' && !inWord)
|
|
814
|
+
break;
|
|
815
|
+
if (ch === "'" || ch === '"') {
|
|
816
|
+
quote = ch;
|
|
817
|
+
inWord = true;
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
if (ch === '\\') {
|
|
821
|
+
const next = value[i + 1];
|
|
822
|
+
if (next === '_') {
|
|
823
|
+
if (inWord)
|
|
824
|
+
tokens.push(word);
|
|
825
|
+
word = '';
|
|
826
|
+
inWord = false;
|
|
827
|
+
}
|
|
828
|
+
else if (next !== undefined && Object.prototype.hasOwnProperty.call(escapes, next)) {
|
|
829
|
+
word += escapes[next];
|
|
830
|
+
inWord = true;
|
|
831
|
+
}
|
|
832
|
+
else
|
|
833
|
+
return null;
|
|
834
|
+
i += 1;
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
word += ch;
|
|
838
|
+
inWord = true;
|
|
839
|
+
}
|
|
840
|
+
if (quote)
|
|
841
|
+
return null;
|
|
842
|
+
if (inWord)
|
|
843
|
+
tokens.push(word);
|
|
844
|
+
return tokens;
|
|
845
|
+
}
|
|
846
|
+
/** The words after a shell interpreter: only plain option flags are understood; `-c <command>` is returned as a command. */
|
|
847
|
+
function classifyShellArguments(tokens) {
|
|
848
|
+
let command = false;
|
|
849
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
850
|
+
const token = tokens[i];
|
|
851
|
+
if (token === '--')
|
|
852
|
+
return i + 1 < tokens.length || command ? { kind: 'unknown' } : { kind: 'shell' };
|
|
853
|
+
if (!/^[-+]./.test(token)) {
|
|
854
|
+
// An operand: with -c it is the command string (the rest are $0, $1 ...); otherwise a script other than this file.
|
|
855
|
+
return command ? { kind: 'other', command: token } : { kind: 'unknown' };
|
|
856
|
+
}
|
|
857
|
+
if (token.startsWith('--')) {
|
|
858
|
+
if (!SHELL_LONG_FLAGS.has(token))
|
|
859
|
+
return { kind: 'unknown' };
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
const letters = token.slice(1);
|
|
863
|
+
for (let j = 0; j < letters.length; j += 1) {
|
|
864
|
+
const letter = letters[j];
|
|
865
|
+
if (letter === 'c') {
|
|
866
|
+
command = true;
|
|
867
|
+
}
|
|
868
|
+
else if (letter === 'o') {
|
|
869
|
+
// `-o name` (also `-euo pipefail`): the name is the next word.
|
|
870
|
+
if (j !== letters.length - 1)
|
|
871
|
+
return { kind: 'unknown' };
|
|
872
|
+
const name = tokens[i + 1];
|
|
873
|
+
if (name === undefined || !SHELL_OPTION_NAMES.has(name))
|
|
874
|
+
return { kind: 'unknown' };
|
|
875
|
+
i += 1;
|
|
876
|
+
}
|
|
877
|
+
else if (!SHELL_FLAG_LETTERS.includes(letter))
|
|
878
|
+
return { kind: 'unknown' };
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return command ? { kind: 'unknown' } : { kind: 'shell' };
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* What runs after `env`: options, then the program. Anything but plain
|
|
885
|
+
* `-i`/`-u NAME`/`-S` and a program is unknown (NAME=value, -C, --argv0 ...).
|
|
886
|
+
* `tokens` are argv words; `raw` (with each word's end offset) is the shebang
|
|
887
|
+
* text itself, which `-S` consumes verbatim and splits with env's own quoting.
|
|
888
|
+
*/
|
|
889
|
+
function walkEnv(tokens, ends, raw, depth, flags) {
|
|
890
|
+
if (depth > MAX_ENV_SPLIT_DEPTH)
|
|
891
|
+
return { kind: 'unknown' };
|
|
892
|
+
const split = (attached, index) => {
|
|
893
|
+
let value;
|
|
894
|
+
let consumed = index;
|
|
895
|
+
if (ends)
|
|
896
|
+
value = attached + raw.slice(ends[index]);
|
|
897
|
+
else if (attached !== '')
|
|
898
|
+
value = attached;
|
|
899
|
+
else {
|
|
900
|
+
if (tokens[index + 1] === undefined)
|
|
901
|
+
return { kind: 'unknown' };
|
|
902
|
+
value = tokens[index + 1];
|
|
903
|
+
consumed = index + 1;
|
|
904
|
+
}
|
|
905
|
+
const words = splitEnvString(value);
|
|
906
|
+
if (!words || words.length === 0)
|
|
907
|
+
return { kind: 'unknown' };
|
|
908
|
+
// An escape (`\r`, `\t`, `\f` ...) can put a control character, tab included, into a word.
|
|
909
|
+
if (words.some((word) => /[^\x20-\x7e]/.test(word)))
|
|
910
|
+
flags.degraded = true;
|
|
911
|
+
return walkEnv(ends ? words : [...words, ...tokens.slice(consumed + 1)], null, '', depth + 1, flags);
|
|
912
|
+
};
|
|
913
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
914
|
+
const word = tokens[i];
|
|
915
|
+
if (word === '--')
|
|
916
|
+
return classifyProgram(tokens.slice(i + 1), depth, flags);
|
|
917
|
+
if (word === '-')
|
|
918
|
+
continue;
|
|
919
|
+
if (word.startsWith('--')) {
|
|
920
|
+
if (word === '--ignore-environment')
|
|
921
|
+
continue;
|
|
922
|
+
// Harmless to env itself, but not on the allowlist: never a definite shell.
|
|
923
|
+
if (word === '--null' || word === '--debug') {
|
|
924
|
+
flags.degraded = true;
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
if (word === '--unset') {
|
|
928
|
+
if (tokens[i + 1] === undefined)
|
|
929
|
+
return { kind: 'unknown' };
|
|
930
|
+
i += 1;
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (word.startsWith('--unset='))
|
|
934
|
+
continue;
|
|
935
|
+
if (word === '--split-string')
|
|
936
|
+
return split('', i);
|
|
937
|
+
if (word.startsWith('--split-string='))
|
|
938
|
+
return split(word.slice('--split-string='.length), i);
|
|
939
|
+
return { kind: 'unknown' };
|
|
940
|
+
}
|
|
941
|
+
if (word.startsWith('-')) {
|
|
942
|
+
for (let j = 1; j < word.length; j += 1) {
|
|
943
|
+
const letter = word[j];
|
|
944
|
+
if (letter === 'i')
|
|
945
|
+
continue;
|
|
946
|
+
if (letter === '0' || letter === 'v') {
|
|
947
|
+
flags.degraded = true;
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
if (letter === 'u') {
|
|
951
|
+
// The name is the rest of this word or the next one.
|
|
952
|
+
if (j === word.length - 1) {
|
|
953
|
+
if (tokens[i + 1] === undefined)
|
|
954
|
+
return { kind: 'unknown' };
|
|
955
|
+
i += 1;
|
|
956
|
+
}
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
if (letter === 'S')
|
|
960
|
+
return split(word.slice(j + 1), i);
|
|
961
|
+
return { kind: 'unknown' };
|
|
962
|
+
}
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
// NAME=value (BASH_ENV, ENV, PATH, SHELLOPTS, PS4 ...): the interpreter's environment is not modelled.
|
|
966
|
+
if (word.includes('='))
|
|
967
|
+
return { kind: 'unknown' };
|
|
968
|
+
return classifyProgram(tokens.slice(i), depth, flags);
|
|
969
|
+
}
|
|
970
|
+
return { kind: 'unknown' };
|
|
971
|
+
}
|
|
972
|
+
function classifyEnvArguments(raw, flags) {
|
|
973
|
+
const tokens = [];
|
|
974
|
+
const ends = [];
|
|
975
|
+
const re = /[^ \t]+/g;
|
|
976
|
+
for (let match = re.exec(raw); match; match = re.exec(raw)) {
|
|
977
|
+
tokens.push(match[0]);
|
|
978
|
+
ends.push(match.index + match[0].length);
|
|
979
|
+
}
|
|
980
|
+
return walkEnv(tokens, ends, raw, 0, flags);
|
|
981
|
+
}
|
|
982
|
+
function classifyProgram(tokens, depth, flags) {
|
|
983
|
+
const program = tokens[0];
|
|
984
|
+
if (program === undefined || program === '')
|
|
985
|
+
return { kind: 'unknown' };
|
|
986
|
+
const slash = program.lastIndexOf('/');
|
|
987
|
+
const base = slash < 0 ? program : program.slice(slash + 1);
|
|
988
|
+
if (base !== 'env' && !SHELL_PROGRAMS.test(base))
|
|
989
|
+
return { kind: 'other' };
|
|
990
|
+
// A shell or env named by a relative path, or found outside the system bin
|
|
991
|
+
// dirs, is a file the agent may have planted: its name proves nothing.
|
|
992
|
+
if (slash >= 0 && !SYSTEM_BIN_DIRS.has(slash === 0 ? '/' : program.slice(0, slash)))
|
|
993
|
+
return { kind: 'unknown' };
|
|
994
|
+
if (base === 'env')
|
|
995
|
+
return walkEnv(tokens.slice(1), null, '', depth + 1, flags);
|
|
996
|
+
return classifyShellArguments(tokens.slice(1));
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* `#!` line → what really runs. 'shell' only for a system shell with plain flags
|
|
1000
|
+
* on a plain line; every form that is not understood is 'unknown', and a line
|
|
1001
|
+
* that is understood but not plain (control or non-ASCII characters, too long,
|
|
1002
|
+
* an env option off the allowlist) is 'other', never 'shell'.
|
|
1003
|
+
*/
|
|
1004
|
+
function classifyShebang(line) {
|
|
1005
|
+
const body = line.trim();
|
|
1006
|
+
const space = body.search(/[ \t]/);
|
|
1007
|
+
const interpreter = space < 0 ? body : body.slice(0, space);
|
|
1008
|
+
const rest = space < 0 ? '' : body.slice(space).trim();
|
|
1009
|
+
if (!interpreter.startsWith('/'))
|
|
1010
|
+
return { kind: 'unknown' };
|
|
1011
|
+
const slash = interpreter.lastIndexOf('/');
|
|
1012
|
+
const dir = interpreter.slice(0, slash) || '/';
|
|
1013
|
+
const base = interpreter.slice(slash + 1);
|
|
1014
|
+
let result;
|
|
1015
|
+
const flags = { degraded: false };
|
|
1016
|
+
if (base === 'env' || SHELL_PROGRAMS.test(base)) {
|
|
1017
|
+
if (!SYSTEM_BIN_DIRS.has(dir))
|
|
1018
|
+
return { kind: 'unknown' };
|
|
1019
|
+
if (base === 'env')
|
|
1020
|
+
result = classifyEnvArguments(rest, flags);
|
|
1021
|
+
else
|
|
1022
|
+
result = classifyShellArguments(rest.split(/[ \t]+/).filter(Boolean));
|
|
1023
|
+
}
|
|
1024
|
+
else
|
|
1025
|
+
result = { kind: 'other' };
|
|
1026
|
+
// The kernel ends the line at \n only; \r and the other control characters stay in it.
|
|
1027
|
+
if (line.length + 2 > MAX_SHEBANG_LINE || !PLAIN_SHEBANG_LINE.test(line))
|
|
1028
|
+
flags.degraded = true;
|
|
1029
|
+
return flags.degraded ? degrade(result) : result;
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* A script file run as `./x.sh` or `bash x.sh` (≤64 KB), realpath-ed; the
|
|
1033
|
+
* interpreter comes from the shebang, which is the first line up to `\n` only.
|
|
1034
|
+
* With no shebang the kernel fails the exec and the invoking shell runs the file
|
|
1035
|
+
* as a sh script, unless its first line carries binary magic (control characters).
|
|
1036
|
+
*/
|
|
1037
|
+
function readScriptFile(fs, path) {
|
|
1038
|
+
const read = readResolved(fs, path, exports.MAX_SCRIPT_BYTES);
|
|
1039
|
+
if (read.status !== 'ok')
|
|
1040
|
+
return read;
|
|
1041
|
+
const newline = read.text.indexOf('\n');
|
|
1042
|
+
const firstLine = newline < 0 ? read.text : read.text.slice(0, newline);
|
|
1043
|
+
let result;
|
|
1044
|
+
if (read.text.startsWith('#!')) {
|
|
1045
|
+
const line = firstLine.slice(2);
|
|
1046
|
+
result = classifyShebang(line);
|
|
1047
|
+
// Never looser than the answer this parser gave when it cut the line at its first \r (round 3 and before).
|
|
1048
|
+
const cr = line.indexOf('\r');
|
|
1049
|
+
if (cr >= 0 && result.kind !== 'unknown' && classifyShebang(line.slice(0, cr)).kind === 'unknown')
|
|
1050
|
+
result = { kind: 'unknown' };
|
|
1051
|
+
}
|
|
1052
|
+
else
|
|
1053
|
+
result = CONTROL_CHARACTER.test(firstLine) ? { kind: 'other' } : { kind: 'shell' };
|
|
1054
|
+
if (result.kind === 'unknown')
|
|
1055
|
+
return { status: 'unknown', reason: 'shebang' };
|
|
1056
|
+
if (result.kind === 'shell')
|
|
1057
|
+
return { status: 'ok', path: read.path, interpreter: 'shell', text: read.text };
|
|
1058
|
+
return { status: 'ok', path: read.path, interpreter: 'other', text: read.text, ...(result.command !== undefined ? { shebangCommand: result.command } : {}) };
|
|
1059
|
+
}
|
|
1060
|
+
//# sourceMappingURL=seatbelt-scripts.js.map
|