@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,455 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// A conservative POSIX-shell parser for classification. It turns a command string into simple
|
|
4
|
+
// commands (argv words, assignments, redirects, heredoc bodies) and records every construct it
|
|
5
|
+
// cannot resolve statically (variables, command/process substitution, eval-like forms). The
|
|
6
|
+
// classifier treats anything dynamic as not routine, so a parse gap can only add a server check,
|
|
7
|
+
// never skip one (ADV-06 H1-H3).
|
|
8
|
+
|
|
9
|
+
const MAX_SOURCE_BYTES = 64 * 1024;
|
|
10
|
+
const MAX_COMMANDS = 256;
|
|
11
|
+
|
|
12
|
+
const SEPARATOR_KEYWORDS = new Set(['if', 'then', 'else', 'elif', 'fi', 'do', 'done', 'while', 'until', '!', '{', '}', 'time']);
|
|
13
|
+
|
|
14
|
+
class ParseError extends Error {}
|
|
15
|
+
|
|
16
|
+
function decodeAnsiC(body) {
|
|
17
|
+
let out = '';
|
|
18
|
+
for (let i = 0; i < body.length; i += 1) {
|
|
19
|
+
const ch = body[i];
|
|
20
|
+
if (ch !== '\\' || i === body.length - 1) { out += ch; continue; }
|
|
21
|
+
const next = body[i + 1];
|
|
22
|
+
i += 1;
|
|
23
|
+
const simple = { n: '\n', t: '\t', r: '\r', a: '\x07', b: '\b', e: '\x1b', E: '\x1b', f: '\f', v: '\v', '\\': '\\', "'": "'", '"': '"', '?': '?' };
|
|
24
|
+
if (Object.prototype.hasOwnProperty.call(simple, next)) { out += simple[next]; continue; }
|
|
25
|
+
if (next === 'x') {
|
|
26
|
+
const match = body.slice(i + 1).match(/^[0-9a-fA-F]{1,2}/);
|
|
27
|
+
if (match) { out += String.fromCharCode(parseInt(match[0], 16)); i += match[0].length; continue; }
|
|
28
|
+
}
|
|
29
|
+
if (next === 'u' || next === 'U') {
|
|
30
|
+
const match = body.slice(i + 1).match(next === 'u' ? /^[0-9a-fA-F]{1,4}/ : /^[0-9a-fA-F]{1,8}/);
|
|
31
|
+
if (match) { out += String.fromCodePoint(parseInt(match[0], 16)); i += match[0].length; continue; }
|
|
32
|
+
}
|
|
33
|
+
if (/[0-7]/.test(next)) {
|
|
34
|
+
const match = body.slice(i).match(/^[0-7]{1,3}/);
|
|
35
|
+
out += String.fromCharCode(parseInt(match[0], 8));
|
|
36
|
+
i += match[0].length - 1;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (next === 'c') { i += 1; continue; }
|
|
40
|
+
out += `\\${next}`;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Returns the index just past the matching close paren for a "(" at `start - 1`, honouring
|
|
46
|
+
// quotes and nested parentheses. Throws on an unterminated group.
|
|
47
|
+
function scanBalanced(src, start, open = '(', close = ')') {
|
|
48
|
+
let depth = 1;
|
|
49
|
+
let i = start;
|
|
50
|
+
while (i < src.length) {
|
|
51
|
+
const ch = src[i];
|
|
52
|
+
if (ch === '\\') { i += 2; continue; }
|
|
53
|
+
if (ch === '<' && src[i + 1] === '<' && src[i + 2] !== '<') {
|
|
54
|
+
// Heredoc inside a substitution: skip its body so quotes in the text (for example an
|
|
55
|
+
// apostrophe in a commit message) do not unbalance the scan.
|
|
56
|
+
const match = src.slice(i).match(/^<<-?\s*(['"]?)([A-Za-z0-9_.-]+)\1/);
|
|
57
|
+
if (match) {
|
|
58
|
+
const bodyStart = src.indexOf('\n', i + match[0].length);
|
|
59
|
+
if (bodyStart < 0) throw new ParseError('unterminated heredoc');
|
|
60
|
+
const stripTabs = src[i + 2] === '-';
|
|
61
|
+
let j = bodyStart + 1;
|
|
62
|
+
let found = false;
|
|
63
|
+
while (j <= src.length) {
|
|
64
|
+
let end = src.indexOf('\n', j);
|
|
65
|
+
if (end < 0) end = src.length;
|
|
66
|
+
const line = stripTabs ? src.slice(j, end).replace(/^\t+/, '') : src.slice(j, end);
|
|
67
|
+
j = end + 1;
|
|
68
|
+
if (line === match[2]) { found = true; break; }
|
|
69
|
+
if (end >= src.length) break;
|
|
70
|
+
}
|
|
71
|
+
if (!found) throw new ParseError('unterminated heredoc');
|
|
72
|
+
i = j - 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (ch === "'") {
|
|
77
|
+
const end = src.indexOf("'", i + 1);
|
|
78
|
+
if (end < 0) throw new ParseError('unterminated quote');
|
|
79
|
+
i = end + 1;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (ch === '"') {
|
|
83
|
+
i += 1;
|
|
84
|
+
while (i < src.length && src[i] !== '"') i += src[i] === '\\' ? 2 : 1;
|
|
85
|
+
if (i >= src.length) throw new ParseError('unterminated quote');
|
|
86
|
+
i += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (ch === '`') {
|
|
90
|
+
const end = src.indexOf('`', i + 1);
|
|
91
|
+
if (end < 0) throw new ParseError('unterminated backtick');
|
|
92
|
+
i = end + 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (ch === open) depth += 1;
|
|
96
|
+
else if (ch === close) {
|
|
97
|
+
depth -= 1;
|
|
98
|
+
if (depth === 0) return i + 1;
|
|
99
|
+
}
|
|
100
|
+
i += 1;
|
|
101
|
+
}
|
|
102
|
+
throw new ParseError('unterminated group');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function newWord() {
|
|
106
|
+
return { text: '', dynamic: false, quoted: false, substitution: false, started: false, variables: [], slots: [] };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function tokenize(src, state) {
|
|
110
|
+
const tokens = [];
|
|
111
|
+
let word = newWord();
|
|
112
|
+
const pendingHeredocs = [];
|
|
113
|
+
let i = 0;
|
|
114
|
+
|
|
115
|
+
const pushWord = () => {
|
|
116
|
+
if (word.started) tokens.push({ type: 'word', word });
|
|
117
|
+
word = newWord();
|
|
118
|
+
};
|
|
119
|
+
const pushOp = (op) => { pushWord(); tokens.push({ type: 'op', op }); };
|
|
120
|
+
const markSubstitution = (inner) => {
|
|
121
|
+
word.dynamic = true;
|
|
122
|
+
word.substitution = true;
|
|
123
|
+
word.started = true;
|
|
124
|
+
word.slots.push({ type: 'sub' });
|
|
125
|
+
state.substitutions.push(inner);
|
|
126
|
+
state.reasons.add('command_substitution');
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const readHeredocBodies = () => {
|
|
130
|
+
while (pendingHeredocs.length > 0) {
|
|
131
|
+
const heredoc = pendingHeredocs.shift();
|
|
132
|
+
const lines = [];
|
|
133
|
+
let found = false;
|
|
134
|
+
while (i < src.length) {
|
|
135
|
+
let end = src.indexOf('\n', i);
|
|
136
|
+
if (end < 0) end = src.length;
|
|
137
|
+
let line = src.slice(i, end);
|
|
138
|
+
i = end + 1;
|
|
139
|
+
if (heredoc.stripTabs) line = line.replace(/^\t+/, '');
|
|
140
|
+
if (line === heredoc.delimiter) { found = true; break; }
|
|
141
|
+
lines.push(line);
|
|
142
|
+
}
|
|
143
|
+
heredoc.token.body = lines.join('\n');
|
|
144
|
+
heredoc.token.bodyDynamic = !heredoc.quoted && /[$`]/.test(heredoc.token.body);
|
|
145
|
+
if (!found) state.reasons.add('unterminated_heredoc');
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
while (i < src.length) {
|
|
150
|
+
const ch = src[i];
|
|
151
|
+
|
|
152
|
+
if (ch === '\n') {
|
|
153
|
+
pushOp(';');
|
|
154
|
+
i += 1;
|
|
155
|
+
readHeredocBodies();
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (ch === ' ' || ch === '\t' || ch === '\r') { pushWord(); i += 1; continue; }
|
|
159
|
+
if (ch === '#' && !word.started) {
|
|
160
|
+
const end = src.indexOf('\n', i);
|
|
161
|
+
i = end < 0 ? src.length : end;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (ch === '\\') {
|
|
165
|
+
if (src[i + 1] === '\n') { i += 2; continue; }
|
|
166
|
+
if (i + 1 < src.length) { word.text += src[i + 1]; word.started = true; word.quoted = true; }
|
|
167
|
+
i += 2;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (ch === "'") {
|
|
171
|
+
const end = src.indexOf("'", i + 1);
|
|
172
|
+
if (end < 0) throw new ParseError('unterminated single quote');
|
|
173
|
+
word.text += src.slice(i + 1, end);
|
|
174
|
+
word.started = true;
|
|
175
|
+
word.quoted = true;
|
|
176
|
+
i = end + 1;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (ch === '$' && src[i + 1] === "'") {
|
|
180
|
+
let j = i + 2;
|
|
181
|
+
while (j < src.length && src[j] !== "'") j += src[j] === '\\' ? 2 : 1;
|
|
182
|
+
if (j >= src.length) throw new ParseError('unterminated ANSI-C quote');
|
|
183
|
+
word.text += decodeAnsiC(src.slice(i + 2, j));
|
|
184
|
+
word.started = true;
|
|
185
|
+
word.quoted = true;
|
|
186
|
+
state.reasons.add('ansi_c_quote');
|
|
187
|
+
i = j + 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (ch === '"') {
|
|
191
|
+
let j = i + 1;
|
|
192
|
+
word.started = true;
|
|
193
|
+
word.quoted = true;
|
|
194
|
+
while (j < src.length && src[j] !== '"') {
|
|
195
|
+
const c = src[j];
|
|
196
|
+
if (c === '\\' && j + 1 < src.length) {
|
|
197
|
+
const n = src[j + 1];
|
|
198
|
+
if (n === '$' || n === '`' || n === '"' || n === '\\') { word.text += n; j += 2; continue; }
|
|
199
|
+
if (n === '\n') { j += 2; continue; }
|
|
200
|
+
word.text += c;
|
|
201
|
+
j += 1;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (c === '$' && src[j + 1] === '(') {
|
|
205
|
+
if (src[j + 2] === '(') {
|
|
206
|
+
const end = scanBalanced(src, j + 3);
|
|
207
|
+
word.text += '\u0000';
|
|
208
|
+
word.slots.push({ type: 'arith' });
|
|
209
|
+
word.dynamic = true;
|
|
210
|
+
j = end + 1;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const end = scanBalanced(src, j + 2);
|
|
214
|
+
markSubstitution(src.slice(j + 2, end - 1));
|
|
215
|
+
word.text += '\u0000';
|
|
216
|
+
j = end;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (c === '`') {
|
|
220
|
+
const end = src.indexOf('`', j + 1);
|
|
221
|
+
if (end < 0) throw new ParseError('unterminated backtick');
|
|
222
|
+
markSubstitution(src.slice(j + 1, end));
|
|
223
|
+
word.text += '\u0000';
|
|
224
|
+
j = end + 1;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (c === '$' && /[A-Za-z_{0-9@*#?$!-]/.test(src[j + 1] || '')) {
|
|
228
|
+
const match = src.slice(j + 1).match(/^(\{[^}]*\}|[A-Za-z_][A-Za-z0-9_]*|[0-9@*#?$!-])/);
|
|
229
|
+
const name = match ? match[0].replace(/^\{|\}$/g, '') : '?';
|
|
230
|
+
word.dynamic = true;
|
|
231
|
+
word.variables.push(name);
|
|
232
|
+
word.slots.push({ type: 'var', name });
|
|
233
|
+
word.text += '\u0000';
|
|
234
|
+
j += 1 + (match ? match[0].length : 0);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
word.text += c;
|
|
238
|
+
j += 1;
|
|
239
|
+
}
|
|
240
|
+
if (j >= src.length) throw new ParseError('unterminated double quote');
|
|
241
|
+
i = j + 1;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (ch === '`') {
|
|
245
|
+
const end = src.indexOf('`', i + 1);
|
|
246
|
+
if (end < 0) throw new ParseError('unterminated backtick');
|
|
247
|
+
markSubstitution(src.slice(i + 1, end));
|
|
248
|
+
word.text += '\u0000';
|
|
249
|
+
i = end + 1;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (ch === '$') {
|
|
253
|
+
const next = src[i + 1] || '';
|
|
254
|
+
if (next === '(' && src[i + 2] === '(') {
|
|
255
|
+
const end = scanBalanced(src, i + 3);
|
|
256
|
+
word.text += '\u0000';
|
|
257
|
+
word.slots.push({ type: 'arith' });
|
|
258
|
+
word.dynamic = true;
|
|
259
|
+
word.started = true;
|
|
260
|
+
i = end + 1;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (next === '(') {
|
|
264
|
+
const end = scanBalanced(src, i + 2);
|
|
265
|
+
markSubstitution(src.slice(i + 2, end - 1));
|
|
266
|
+
word.text += '\u0000';
|
|
267
|
+
i = end;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const match = src.slice(i + 1).match(/^(\{[^}]*\}|[A-Za-z_][A-Za-z0-9_]*|[0-9@*#?$!-])/);
|
|
271
|
+
if (match) {
|
|
272
|
+
const name = match[0].replace(/^\{|\}$/g, '');
|
|
273
|
+
word.dynamic = true;
|
|
274
|
+
word.started = true;
|
|
275
|
+
word.variables.push(name);
|
|
276
|
+
word.slots.push({ type: 'var', name });
|
|
277
|
+
word.text += '\u0000';
|
|
278
|
+
i += 1 + match[0].length;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
word.text += '$';
|
|
282
|
+
word.started = true;
|
|
283
|
+
i += 1;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if ((ch === '<' || ch === '>') && src[i + 1] === '(' && !word.started) {
|
|
287
|
+
const end = scanBalanced(src, i + 2);
|
|
288
|
+
markSubstitution(src.slice(i + 2, end - 1));
|
|
289
|
+
state.reasons.add('process_substitution');
|
|
290
|
+
word.text += '\u0000';
|
|
291
|
+
i = end;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (ch === '<' || ch === '>' || (ch === '&' && src[i + 1] === '>')) {
|
|
295
|
+
let fd = null;
|
|
296
|
+
if (word.started && !word.quoted && /^[0-9]+$/.test(word.text)) { fd = word.text; word = newWord(); }
|
|
297
|
+
pushWord();
|
|
298
|
+
const rest = src.slice(i);
|
|
299
|
+
const op = (rest.match(/^(&>>|&>|<<<|<<-|<<|<>|>>|>\||>&|<&|<|>)/) || [ch])[0];
|
|
300
|
+
i += op.length;
|
|
301
|
+
tokens.push({ type: 'redir', op, fd });
|
|
302
|
+
if (op === '<<' || op === '<<-') {
|
|
303
|
+
// Read the delimiter word now so the body can be collected after the next newline.
|
|
304
|
+
while (src[i] === ' ' || src[i] === '\t') i += 1;
|
|
305
|
+
let delimiter = '';
|
|
306
|
+
let quoted = false;
|
|
307
|
+
while (i < src.length && !/[\s;&|<>()]/.test(src[i])) {
|
|
308
|
+
if (src[i] === "'" || src[i] === '"') {
|
|
309
|
+
const q = src[i];
|
|
310
|
+
const end = src.indexOf(q, i + 1);
|
|
311
|
+
if (end < 0) throw new ParseError('unterminated heredoc delimiter');
|
|
312
|
+
delimiter += src.slice(i + 1, end);
|
|
313
|
+
quoted = true;
|
|
314
|
+
i = end + 1;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (src[i] === '\\') { delimiter += src[i + 1] || ''; quoted = true; i += 2; continue; }
|
|
318
|
+
delimiter += src[i];
|
|
319
|
+
i += 1;
|
|
320
|
+
}
|
|
321
|
+
const heredocToken = { type: 'heredoc', delimiter, body: '', bodyDynamic: false };
|
|
322
|
+
tokens.push(heredocToken);
|
|
323
|
+
pendingHeredocs.push({ delimiter, quoted, stripTabs: op === '<<-', token: heredocToken });
|
|
324
|
+
}
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (ch === '|' || ch === '&' || ch === ';' || ch === '(' || ch === ')') {
|
|
328
|
+
const rest = src.slice(i);
|
|
329
|
+
const op = (rest.match(/^(\|\||\|&|&&|;;&|;;|;&|\||&|;|\(|\))/) || [ch])[0];
|
|
330
|
+
if (op === ';;' || op === ';&' || op === ';;&') state.reasons.add('case_statement');
|
|
331
|
+
pushOp(op);
|
|
332
|
+
i += op.length;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if ((ch === '*' || ch === '?' || ch === '[') && !word.quoted) word.glob = true;
|
|
336
|
+
word.text += ch;
|
|
337
|
+
word.started = true;
|
|
338
|
+
i += 1;
|
|
339
|
+
}
|
|
340
|
+
pushWord();
|
|
341
|
+
if (pendingHeredocs.length) readHeredocBodies();
|
|
342
|
+
return tokens;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function newCommand() {
|
|
346
|
+
return { words: [], assignments: [], redirects: [], heredoc: null, heredocDynamic: false, pipedInput: false, pipesOutput: false, background: false, subshell: false };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function parseShell(source, options = {}) {
|
|
350
|
+
const depth = options.depth || 0;
|
|
351
|
+
const state = { substitutions: [], reasons: new Set() };
|
|
352
|
+
const result = { ok: true, commands: [], substitutions: state.substitutions, reasons: [], dynamic: false, error: null };
|
|
353
|
+
const text = String(source == null ? '' : source);
|
|
354
|
+
if (Buffer.byteLength(text) > MAX_SOURCE_BYTES) {
|
|
355
|
+
return { ...result, ok: false, dynamic: true, error: 'too_large', reasons: ['too_large'] };
|
|
356
|
+
}
|
|
357
|
+
let tokens;
|
|
358
|
+
try {
|
|
359
|
+
tokens = tokenize(text, state);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
return { ...result, ok: false, dynamic: true, error: error instanceof ParseError ? error.message : 'parse_error', reasons: ['unparseable'] };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
let current = newCommand();
|
|
365
|
+
let forState = null;
|
|
366
|
+
let subshellDepth = 0;
|
|
367
|
+
let expectRedirTarget = null;
|
|
368
|
+
const finish = (op) => {
|
|
369
|
+
if (current.words.length || current.assignments.length || current.redirects.length || current.heredoc !== null) {
|
|
370
|
+
if (op === '|' || op === '|&') current.pipesOutput = true;
|
|
371
|
+
if (op === '&') current.background = true;
|
|
372
|
+
current.subshell = subshellDepth > 0;
|
|
373
|
+
result.commands.push(current);
|
|
374
|
+
}
|
|
375
|
+
const next = newCommand();
|
|
376
|
+
if (op === '|' || op === '|&') next.pipedInput = true;
|
|
377
|
+
current = next;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
for (let t = 0; t < tokens.length; t += 1) {
|
|
381
|
+
const token = tokens[t];
|
|
382
|
+
if (expectRedirTarget) {
|
|
383
|
+
if (token.type === 'word') {
|
|
384
|
+
expectRedirTarget.target = token.word;
|
|
385
|
+
expectRedirTarget = null;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (token.type === 'heredoc') { expectRedirTarget = null; }
|
|
389
|
+
else { state.reasons.add('dangling_redirect'); expectRedirTarget = null; }
|
|
390
|
+
}
|
|
391
|
+
if (token.type === 'heredoc') {
|
|
392
|
+
current.heredoc = token.body;
|
|
393
|
+
current.heredocDynamic = token.bodyDynamic;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (token.type === 'redir') {
|
|
397
|
+
const redirect = { op: token.op, fd: token.fd, target: null };
|
|
398
|
+
current.redirects.push(redirect);
|
|
399
|
+
if (token.op !== '<<' && token.op !== '<<-') expectRedirTarget = redirect;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (token.type === 'op') {
|
|
403
|
+
if (token.op === '(') {
|
|
404
|
+
// `name ( )` defines a function; otherwise a subshell group.
|
|
405
|
+
if (current.words.length === 1 && tokens[t + 1] && tokens[t + 1].type === 'op' && tokens[t + 1].op === ')') {
|
|
406
|
+
current.words = [];
|
|
407
|
+
t += 1;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
finish(';');
|
|
411
|
+
subshellDepth += 1;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (token.op === ')') { finish(';'); subshellDepth = Math.max(0, subshellDepth - 1); continue; }
|
|
415
|
+
finish(token.op);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const word = token.word;
|
|
419
|
+
if (forState) {
|
|
420
|
+
// `for NAME [in ITEMS...]; do`: record the loop variable and its literal items so the
|
|
421
|
+
// classifier can check every value the body will see.
|
|
422
|
+
if (!word.quoted && word.text === 'do') {
|
|
423
|
+
result.commands.push({ ...newCommand(), forLoop: { name: forState.name, items: forState.sawIn ? forState.items : null } });
|
|
424
|
+
forState = null;
|
|
425
|
+
} else if (forState.name === null) forState.name = word.text;
|
|
426
|
+
else if (!forState.sawIn && !word.quoted && word.text === 'in') forState.sawIn = true;
|
|
427
|
+
else if (forState.sawIn) forState.items.push(word);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (current.words.length === 0) {
|
|
431
|
+
if (!word.quoted && SEPARATOR_KEYWORDS.has(word.text)) continue;
|
|
432
|
+
if (!word.quoted && (word.text === 'for' || word.text === 'select')) { forState = { name: null, items: [], sawIn: false }; continue; }
|
|
433
|
+
if (!word.quoted && (word.text === 'case' || word.text === 'coproc')) { state.reasons.add(`${word.text}_statement`); }
|
|
434
|
+
if (!word.quoted && word.text === 'function') { t += 1; continue; }
|
|
435
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*(\[[^\]]*\])?\+?=/.test(word.text)) {
|
|
436
|
+
current.assignments.push(word);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
current.words.push(word);
|
|
441
|
+
}
|
|
442
|
+
if (expectRedirTarget) state.reasons.add('dangling_redirect');
|
|
443
|
+
finish(';');
|
|
444
|
+
|
|
445
|
+
if (result.commands.length > MAX_COMMANDS) {
|
|
446
|
+
return { ...result, ok: false, dynamic: true, error: 'too_many_commands', reasons: ['too_many_commands'] };
|
|
447
|
+
}
|
|
448
|
+
result.reasons = [...state.reasons];
|
|
449
|
+
result.dynamic = result.reasons.length > 0 || result.commands.some((command) => command.words.some((w) => w.dynamic)
|
|
450
|
+
|| command.redirects.some((r) => r.target && r.target.dynamic));
|
|
451
|
+
result.depth = depth;
|
|
452
|
+
return result;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
module.exports = { parseShell, decodeAnsiC, MAX_SOURCE_BYTES };
|