@blogic-cz/agent-tools 1.10.1 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -44
- package/dist/credential-guard/bash.d.ts +3 -0
- package/dist/credential-guard/bash.d.ts.map +1 -0
- package/dist/credential-guard/claude-hook.d.ts.map +1 -1
- package/dist/credential-guard/engine.test.d.ts +5 -0
- package/dist/credential-guard/engine.test.d.ts.map +1 -0
- package/dist/credential-guard/index.d.ts +8 -35
- package/dist/credential-guard/index.d.ts.map +1 -1
- package/dist/credential-guard/ir.d.ts +150 -0
- package/dist/credential-guard/ir.d.ts.map +1 -0
- package/dist/credential-guard/literal-output.d.ts +2 -0
- package/dist/credential-guard/literal-output.d.ts.map +1 -0
- package/dist/credential-guard/passive-awk.d.ts +2 -0
- package/dist/credential-guard/passive-awk.d.ts.map +1 -0
- package/dist/credential-guard/passive-jq.d.ts.map +1 -1
- package/dist/credential-guard/passive-sed.d.ts +2 -0
- package/dist/credential-guard/passive-sed.d.ts.map +1 -0
- package/dist/credential-guard/passive.test.d.ts +2 -0
- package/dist/credential-guard/passive.test.d.ts.map +1 -0
- package/dist/credential-guard/paths.d.ts +14 -0
- package/dist/credential-guard/paths.d.ts.map +1 -0
- package/dist/credential-guard/policy.d.ts +47 -0
- package/dist/credential-guard/policy.d.ts.map +1 -0
- package/dist/credential-guard/powershell.d.ts +3 -0
- package/dist/credential-guard/powershell.d.ts.map +1 -0
- package/dist/credential-guard/registry.d.ts +89 -0
- package/dist/credential-guard/registry.d.ts.map +1 -0
- package/dist/credential-guard/review4.test.d.ts +2 -0
- package/dist/credential-guard/review4.test.d.ts.map +1 -0
- package/dist/credential-guard/review7.test.d.ts +2 -0
- package/dist/credential-guard/review7.test.d.ts.map +1 -0
- package/dist/credential-guard/shell-oracle.d.ts +2 -0
- package/dist/credential-guard/shell-oracle.d.ts.map +1 -0
- package/dist/credential-guard/xargs.d.ts +7 -0
- package/dist/credential-guard/xargs.d.ts.map +1 -0
- package/dist/db-tool/service.d.ts +2 -1
- package/dist/db-tool/service.d.ts.map +1 -1
- package/dist/db-tool/sql-client.d.ts +1 -0
- package/dist/db-tool/sql-client.d.ts.map +1 -1
- package/dist/gh-tool/pr/commands.d.ts.map +1 -1
- package/dist/gh-tool/read/api.d.ts +8 -0
- package/dist/gh-tool/read/api.d.ts.map +1 -1
- package/dist/gh-tool/read/cache.d.ts.map +1 -1
- package/dist/gh-tool/read/commands.d.ts +5 -0
- package/dist/gh-tool/read/commands.d.ts.map +1 -1
- package/dist/gh-tool/read/reads.d.ts +38 -0
- package/dist/gh-tool/read/reads.d.ts.map +1 -1
- package/dist/gh-tool/read/telemetry-command.d.ts +7 -0
- package/dist/gh-tool/read/telemetry-command.d.ts.map +1 -0
- package/dist/gh-tool/read/telemetry-store.d.ts +3 -0
- package/dist/gh-tool/read/telemetry-store.d.ts.map +1 -0
- package/dist/gh-tool/read/telemetry.d.ts +59 -0
- package/dist/gh-tool/read/telemetry.d.ts.map +1 -0
- package/dist/gh-tool/read/transport.d.ts.map +1 -1
- package/dist/observability-tool/logs.d.ts +1 -1
- package/dist/observability-tool/logs.d.ts.map +1 -1
- package/dist/observability-tool/metrics.d.ts +1 -1
- package/dist/observability-tool/metrics.d.ts.map +1 -1
- package/dist/observability-tool/shared.d.ts +2 -1
- package/dist/observability-tool/shared.d.ts.map +1 -1
- package/dist/observability-tool/trace.d.ts +1 -1
- package/dist/observability-tool/trace.d.ts.map +1 -1
- package/dist/shared/content-security.d.ts +3 -1
- package/dist/shared/content-security.d.ts.map +1 -1
- package/dist/shared/secret-env.d.ts +22 -0
- package/dist/shared/secret-env.d.ts.map +1 -0
- package/package.json +2 -1
- package/schemas/agent-tools.schema.json +1 -1
- package/src/credential-guard/bash.ts +769 -0
- package/src/credential-guard/claude-hook.ts +14 -14
- package/src/credential-guard/engine.test.ts +2050 -0
- package/src/credential-guard/index.ts +285 -4268
- package/src/credential-guard/ir.ts +139 -0
- package/src/credential-guard/literal-output.ts +8 -0
- package/src/credential-guard/passive-awk.ts +239 -0
- package/src/credential-guard/passive-jq.ts +183 -24
- package/src/credential-guard/passive-sed.ts +78 -0
- package/src/credential-guard/passive.test.ts +141 -0
- package/src/credential-guard/paths.ts +108 -0
- package/src/credential-guard/policy.ts +1576 -0
- package/src/credential-guard/powershell.ts +220 -0
- package/src/credential-guard/registry.ts +4720 -0
- package/src/credential-guard/review4.test.ts +83 -0
- package/src/credential-guard/review7.test.ts +51 -0
- package/src/credential-guard/rg-types.json +389 -0
- package/src/credential-guard/shell-oracle.ts +187 -0
- package/src/credential-guard/xargs.ts +65 -0
- package/src/db-tool/main.ts +2 -0
- package/src/db-tool/service.ts +122 -140
- package/src/db-tool/sql-client.ts +7 -4
- package/src/gh-tool/main.ts +30 -6
- package/src/gh-tool/pr/commands.ts +9 -2
- package/src/gh-tool/pr/core.ts +2 -2
- package/src/gh-tool/read/api.ts +73 -6
- package/src/gh-tool/read/cache.ts +13 -4
- package/src/gh-tool/read/commands.ts +17 -1
- package/src/gh-tool/read/reads.ts +84 -1
- package/src/gh-tool/read/telemetry-command.ts +45 -0
- package/src/gh-tool/read/telemetry-store.ts +85 -0
- package/src/gh-tool/read/telemetry.ts +191 -0
- package/src/gh-tool/read/transport.ts +10 -5
- package/src/gh-tool/workflow.ts +2 -2
- package/src/observability-tool/main.ts +7 -1
- package/src/observability-tool/shared.ts +20 -14
- package/src/shared/content-security.ts +7 -2
- package/src/shared/secret-env.ts +230 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export type Span = { readonly start: number; readonly end: number };
|
|
2
|
+
export type Script = {
|
|
3
|
+
readonly kind: "script";
|
|
4
|
+
readonly dialect: "bash" | "powershell";
|
|
5
|
+
readonly source: string;
|
|
6
|
+
readonly body: List;
|
|
7
|
+
readonly span: Span;
|
|
8
|
+
};
|
|
9
|
+
export type List = { readonly items: readonly AndOr[] };
|
|
10
|
+
export type AndOr = {
|
|
11
|
+
readonly first: Pipeline;
|
|
12
|
+
readonly background: boolean;
|
|
13
|
+
readonly rest: readonly { readonly op: "&&" | "||"; readonly pipeline: Pipeline }[];
|
|
14
|
+
};
|
|
15
|
+
export type Pipeline = {
|
|
16
|
+
readonly negated: boolean;
|
|
17
|
+
readonly stderr: boolean;
|
|
18
|
+
readonly commands: readonly Command[];
|
|
19
|
+
};
|
|
20
|
+
export type Command =
|
|
21
|
+
| SimpleCommand
|
|
22
|
+
| {
|
|
23
|
+
readonly kind: "group";
|
|
24
|
+
readonly subshell: boolean;
|
|
25
|
+
readonly body: List;
|
|
26
|
+
readonly redirects: readonly Redirect[];
|
|
27
|
+
readonly span: Span;
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
readonly kind: "for";
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly items: readonly Word[];
|
|
33
|
+
readonly body: List;
|
|
34
|
+
readonly redirects: readonly Redirect[];
|
|
35
|
+
readonly span: Span;
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
readonly kind: "while";
|
|
39
|
+
readonly condition: List;
|
|
40
|
+
readonly body: List;
|
|
41
|
+
readonly redirects: readonly Redirect[];
|
|
42
|
+
readonly span: Span;
|
|
43
|
+
}
|
|
44
|
+
| {
|
|
45
|
+
readonly kind: "until";
|
|
46
|
+
readonly condition: List;
|
|
47
|
+
readonly body: List;
|
|
48
|
+
readonly redirects: readonly Redirect[];
|
|
49
|
+
readonly span: Span;
|
|
50
|
+
}
|
|
51
|
+
| {
|
|
52
|
+
readonly kind: "if";
|
|
53
|
+
readonly branches: readonly { readonly condition: List; readonly body: List }[];
|
|
54
|
+
readonly otherwise: List | null;
|
|
55
|
+
readonly redirects: readonly Redirect[];
|
|
56
|
+
readonly span: Span;
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
readonly kind: "case";
|
|
60
|
+
readonly subject: Word;
|
|
61
|
+
readonly arms: readonly { readonly patterns: readonly Word[]; readonly body: List }[];
|
|
62
|
+
readonly redirects: readonly Redirect[];
|
|
63
|
+
readonly span: Span;
|
|
64
|
+
}
|
|
65
|
+
| { readonly kind: "test"; readonly words: readonly Word[]; readonly span: Span };
|
|
66
|
+
export type SimpleCommand = {
|
|
67
|
+
readonly kind: "simple";
|
|
68
|
+
readonly assignments: readonly Assignment[];
|
|
69
|
+
readonly argv: readonly Word[];
|
|
70
|
+
readonly redirects: readonly Redirect[];
|
|
71
|
+
readonly span: Span;
|
|
72
|
+
};
|
|
73
|
+
export type Assignment = {
|
|
74
|
+
readonly operation: "replace" | "opaque";
|
|
75
|
+
readonly name: string;
|
|
76
|
+
readonly value: Word;
|
|
77
|
+
readonly exported: boolean;
|
|
78
|
+
};
|
|
79
|
+
export type Word = {
|
|
80
|
+
readonly split: boolean;
|
|
81
|
+
readonly parts: readonly WordPart[];
|
|
82
|
+
readonly span: Span;
|
|
83
|
+
};
|
|
84
|
+
export type WordPart =
|
|
85
|
+
| { readonly kind: "literal"; readonly text: string; readonly quoted: boolean }
|
|
86
|
+
| { readonly kind: "glob"; readonly text: string }
|
|
87
|
+
| { readonly kind: "brace"; readonly alternatives: readonly string[] }
|
|
88
|
+
| {
|
|
89
|
+
readonly kind: "parameter";
|
|
90
|
+
readonly name: string;
|
|
91
|
+
readonly ambient: boolean;
|
|
92
|
+
readonly operator: "plain" | "default" | "alternate" | "length";
|
|
93
|
+
readonly operand: Word | null;
|
|
94
|
+
readonly span: Span;
|
|
95
|
+
}
|
|
96
|
+
| { readonly kind: "special"; readonly name: string }
|
|
97
|
+
| { readonly kind: "command-substitution"; readonly body: Script }
|
|
98
|
+
| { readonly kind: "process-substitution"; readonly body: Script };
|
|
99
|
+
export type Redirect = (
|
|
100
|
+
| {
|
|
101
|
+
readonly kind: "file";
|
|
102
|
+
readonly op: "<" | ">" | ">>" | "<>" | ">|" | "&>" | "&>>";
|
|
103
|
+
readonly target: Word;
|
|
104
|
+
}
|
|
105
|
+
| { readonly kind: "duplicate"; readonly input: boolean; readonly target: string }
|
|
106
|
+
| { readonly kind: "heredoc"; readonly body: Word; readonly literal: boolean }
|
|
107
|
+
| { readonly kind: "herestring"; readonly body: Word }
|
|
108
|
+
) & { readonly fd: number | null };
|
|
109
|
+
export type ParseErrorCode =
|
|
110
|
+
| "syntax"
|
|
111
|
+
| "unterminated-quote"
|
|
112
|
+
| "unterminated-heredoc"
|
|
113
|
+
| "unterminated-substitution"
|
|
114
|
+
| "function-definition"
|
|
115
|
+
| "unsupported-keyword"
|
|
116
|
+
| "indirect-expansion"
|
|
117
|
+
| "unsupported-parameter-operator"
|
|
118
|
+
| "depth-limit"
|
|
119
|
+
| "size-limit"
|
|
120
|
+
| "work-limit";
|
|
121
|
+
export type ParseError = {
|
|
122
|
+
readonly kind: "parse-error";
|
|
123
|
+
readonly code: ParseErrorCode;
|
|
124
|
+
readonly span: Span;
|
|
125
|
+
};
|
|
126
|
+
export const MAX_ANALYSIS_UNITS = 4096;
|
|
127
|
+
export const MAX_PARSE_DEPTH = 8;
|
|
128
|
+
export const MAX_SCRIPT_BYTES = 262144;
|
|
129
|
+
export function literalWord(text: string, span: Span = { start: 0, end: 0 }): Word {
|
|
130
|
+
return { split: false, parts: [{ kind: "literal", text, quoted: true }], span };
|
|
131
|
+
}
|
|
132
|
+
export function wordText(word: Word): string | undefined {
|
|
133
|
+
let text = "";
|
|
134
|
+
for (const part of word.parts) {
|
|
135
|
+
if (part.kind !== "literal") return undefined;
|
|
136
|
+
text += part.text;
|
|
137
|
+
}
|
|
138
|
+
return text;
|
|
139
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function formatLiteralPrintf(argv: readonly string[]): string | null {
|
|
2
|
+
const start = argv[0] === "--" ? 1 : 0;
|
|
3
|
+
const format = argv[start];
|
|
4
|
+
if (format !== "%s" && format !== "%s\\n" && format !== "%s ") return null;
|
|
5
|
+
const suffix = format === "%s\\n" ? "\n" : format === "%s " ? " " : "";
|
|
6
|
+
const args = argv.slice(start + 1);
|
|
7
|
+
return (args.length ? args : [""]).map((word) => word + suffix).join("");
|
|
8
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
export function isPassiveAwkProgram(text: string): boolean {
|
|
2
|
+
if (text.length > 65_536) return false;
|
|
3
|
+
const tokens: string[] = [];
|
|
4
|
+
const reserved = new Set([
|
|
5
|
+
"BEGIN",
|
|
6
|
+
"END",
|
|
7
|
+
"if",
|
|
8
|
+
"else",
|
|
9
|
+
"for",
|
|
10
|
+
"while",
|
|
11
|
+
"do",
|
|
12
|
+
"in",
|
|
13
|
+
"print",
|
|
14
|
+
"printf",
|
|
15
|
+
"next",
|
|
16
|
+
"nextfile",
|
|
17
|
+
"exit",
|
|
18
|
+
"break",
|
|
19
|
+
"continue",
|
|
20
|
+
"delete",
|
|
21
|
+
"function",
|
|
22
|
+
"getline",
|
|
23
|
+
"system",
|
|
24
|
+
"close",
|
|
25
|
+
"fflush",
|
|
26
|
+
]);
|
|
27
|
+
const variables = new Set([
|
|
28
|
+
"NR",
|
|
29
|
+
"FNR",
|
|
30
|
+
"NF",
|
|
31
|
+
"FILENAME",
|
|
32
|
+
"RSTART",
|
|
33
|
+
"RLENGTH",
|
|
34
|
+
"SUBSEP",
|
|
35
|
+
"FS",
|
|
36
|
+
"RS",
|
|
37
|
+
"OFS",
|
|
38
|
+
"ORS",
|
|
39
|
+
"OFMT",
|
|
40
|
+
"CONVFMT",
|
|
41
|
+
]);
|
|
42
|
+
const functions = new Set([
|
|
43
|
+
"length",
|
|
44
|
+
"substr",
|
|
45
|
+
"index",
|
|
46
|
+
"int",
|
|
47
|
+
"tolower",
|
|
48
|
+
"toupper",
|
|
49
|
+
"split",
|
|
50
|
+
"match",
|
|
51
|
+
"sub",
|
|
52
|
+
"gsub",
|
|
53
|
+
"sprintf",
|
|
54
|
+
"sqrt",
|
|
55
|
+
"log",
|
|
56
|
+
"exp",
|
|
57
|
+
]);
|
|
58
|
+
const local = (token: string): boolean =>
|
|
59
|
+
/^[a-z_][A-Za-z0-9_]*$/.test(token) && !reserved.has(token) && !functions.has(token);
|
|
60
|
+
const constant = (token: string): boolean =>
|
|
61
|
+
/^["0-9]/.test(token) || (token.startsWith("/") && token.length > 1);
|
|
62
|
+
const lexer =
|
|
63
|
+
/[ \t\r]+|\n|"(?:[^"\\\n]|\\[^\n])*"|[0-9]+(?:\.[0-9]+)?|[A-Za-z_][A-Za-z0-9_]*|&&|\|\||==|!=|<=|>=|!~|\+\+|--|[+*%/-]=|[${}[\](),;!~+*%<>?:=-]/y;
|
|
64
|
+
let offset = 0;
|
|
65
|
+
let parentheses = 0;
|
|
66
|
+
while (offset < text.length) {
|
|
67
|
+
if (text[offset] === "/") {
|
|
68
|
+
const previous = tokens.at(-1) ?? "";
|
|
69
|
+
const division =
|
|
70
|
+
constant(previous) ||
|
|
71
|
+
variables.has(previous) ||
|
|
72
|
+
functions.has(previous) ||
|
|
73
|
+
local(previous) ||
|
|
74
|
+
[")", "]", "++", "--"].includes(previous);
|
|
75
|
+
if (division) {
|
|
76
|
+
tokens.push(text[offset + 1] === "=" ? "/=" : "/");
|
|
77
|
+
offset += text[offset + 1] === "=" ? 2 : 1;
|
|
78
|
+
} else {
|
|
79
|
+
const start = offset++;
|
|
80
|
+
let closed = false;
|
|
81
|
+
while (offset < text.length) {
|
|
82
|
+
const ch = text[offset++];
|
|
83
|
+
if (ch === "\\") {
|
|
84
|
+
if (offset === text.length || text[offset] === "\n") return false;
|
|
85
|
+
offset++;
|
|
86
|
+
} else if (ch === "\n") return false;
|
|
87
|
+
else if (ch === "/") {
|
|
88
|
+
closed = true;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!closed) return false;
|
|
93
|
+
tokens.push(text.slice(start, offset));
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
lexer.lastIndex = offset;
|
|
97
|
+
const match = lexer.exec(text);
|
|
98
|
+
if (!match) return false;
|
|
99
|
+
offset = lexer.lastIndex;
|
|
100
|
+
const token = match[0];
|
|
101
|
+
if (token === "(") parentheses++;
|
|
102
|
+
if (token === ")") parentheses--;
|
|
103
|
+
if (token === "\n") {
|
|
104
|
+
if (parentheses === 0) tokens.push(";");
|
|
105
|
+
} else if (!/^\s/.test(token)) tokens.push(token);
|
|
106
|
+
}
|
|
107
|
+
if (tokens.length > 4096) return false;
|
|
108
|
+
}
|
|
109
|
+
let position = 0;
|
|
110
|
+
const take = (token: string): boolean => {
|
|
111
|
+
if (tokens[position] !== token) return false;
|
|
112
|
+
position++;
|
|
113
|
+
return true;
|
|
114
|
+
};
|
|
115
|
+
const operators = new Set([
|
|
116
|
+
"&&",
|
|
117
|
+
"||",
|
|
118
|
+
"==",
|
|
119
|
+
"!=",
|
|
120
|
+
"<",
|
|
121
|
+
"<=",
|
|
122
|
+
">",
|
|
123
|
+
">=",
|
|
124
|
+
"~",
|
|
125
|
+
"!~",
|
|
126
|
+
"+",
|
|
127
|
+
"-",
|
|
128
|
+
"*",
|
|
129
|
+
"/",
|
|
130
|
+
"%",
|
|
131
|
+
"in",
|
|
132
|
+
"=",
|
|
133
|
+
"+=",
|
|
134
|
+
"-=",
|
|
135
|
+
"*=",
|
|
136
|
+
"/=",
|
|
137
|
+
"%=",
|
|
138
|
+
]);
|
|
139
|
+
const startsAtom = (): boolean => {
|
|
140
|
+
const token = tokens[position] ?? "";
|
|
141
|
+
return (
|
|
142
|
+
constant(token) ||
|
|
143
|
+
["$", "("].includes(token) ||
|
|
144
|
+
variables.has(token) ||
|
|
145
|
+
local(token) ||
|
|
146
|
+
functions.has(token)
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
const atom = (depth: number): boolean => {
|
|
150
|
+
if (depth > 64) return false;
|
|
151
|
+
if (take("!") || take("-") || take("+") || take("++") || take("--")) return atom(depth + 1);
|
|
152
|
+
if (take("$")) return atom(depth + 1);
|
|
153
|
+
if (take("(")) return expression(depth + 1) && take(")");
|
|
154
|
+
const token = tokens[position] ?? "";
|
|
155
|
+
if (constant(token)) {
|
|
156
|
+
position++;
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
if (variables.has(token) || local(token)) {
|
|
160
|
+
position++;
|
|
161
|
+
if (tokens[position] === "(") return false;
|
|
162
|
+
if (take("[")) {
|
|
163
|
+
if (!expression(depth + 1)) return false;
|
|
164
|
+
while (take(",")) if (!expression(depth + 1)) return false;
|
|
165
|
+
if (!take("]")) return false;
|
|
166
|
+
}
|
|
167
|
+
if (!take("++")) take("--");
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
if (!functions.has(token)) return false;
|
|
171
|
+
position++;
|
|
172
|
+
if (!take("(")) return token === "length";
|
|
173
|
+
if (take(")")) return token === "length";
|
|
174
|
+
if (!expression(depth + 1)) return false;
|
|
175
|
+
while (take(",")) if (!expression(depth + 1)) return false;
|
|
176
|
+
return take(")");
|
|
177
|
+
};
|
|
178
|
+
const expression = (depth: number, printing = false): boolean => {
|
|
179
|
+
if (!atom(depth)) return false;
|
|
180
|
+
while (true) {
|
|
181
|
+
const operator = tokens[position] ?? "";
|
|
182
|
+
if (operators.has(operator)) {
|
|
183
|
+
if (printing && operator === ">") return false;
|
|
184
|
+
position++;
|
|
185
|
+
if (!atom(depth)) return false;
|
|
186
|
+
} else if (take("?")) {
|
|
187
|
+
return expression(depth + 1, printing) && take(":") && expression(depth + 1, printing);
|
|
188
|
+
} else if (startsAtom()) {
|
|
189
|
+
if (!atom(depth)) return false;
|
|
190
|
+
} else return true;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const block = (depth: number): boolean => {
|
|
194
|
+
if (depth > 64 || !take("{")) return false;
|
|
195
|
+
while (!take("}")) {
|
|
196
|
+
if (take(";")) continue;
|
|
197
|
+
if (!statement(depth + 1)) return false;
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
};
|
|
201
|
+
const statement = (depth: number): boolean => {
|
|
202
|
+
if (depth > 64) return false;
|
|
203
|
+
if (tokens[position] === "{") return block(depth + 1);
|
|
204
|
+
if (take("if")) {
|
|
205
|
+
if (!take("(") || !expression(depth + 1) || !take(")") || !statement(depth + 1)) return false;
|
|
206
|
+
return !take("else") || statement(depth + 1);
|
|
207
|
+
}
|
|
208
|
+
if (take("for")) {
|
|
209
|
+
if (!take("(") || !expression(depth + 1)) return false;
|
|
210
|
+
if (take(";")) {
|
|
211
|
+
if (!expression(depth + 1) || !take(";") || !expression(depth + 1)) return false;
|
|
212
|
+
}
|
|
213
|
+
return take(")") && statement(depth + 1);
|
|
214
|
+
}
|
|
215
|
+
if (take("print") || take("printf")) {
|
|
216
|
+
if (![";", "}"].includes(tokens[position] ?? "")) {
|
|
217
|
+
if (!expression(depth + 1, true)) return false;
|
|
218
|
+
while (take(",")) if (!expression(depth + 1, true)) return false;
|
|
219
|
+
}
|
|
220
|
+
} else if (take("exit")) {
|
|
221
|
+
if (![";", "}"].includes(tokens[position] ?? "") && !expression(depth + 1)) return false;
|
|
222
|
+
} else if (!take("next") && !take("break") && !take("continue") && !expression(depth + 1))
|
|
223
|
+
return false;
|
|
224
|
+
return take(";") || tokens[position] === "}" || tokens[position] === "else";
|
|
225
|
+
};
|
|
226
|
+
let rules = 0;
|
|
227
|
+
while (position < tokens.length) {
|
|
228
|
+
if (take(";")) continue;
|
|
229
|
+
if (tokens[position] !== "{") {
|
|
230
|
+
if (!take("BEGIN") && !take("END") && !expression(0)) return false;
|
|
231
|
+
if (take(",") && !expression(0)) return false;
|
|
232
|
+
}
|
|
233
|
+
rules++;
|
|
234
|
+
if (tokens[position] === "{") {
|
|
235
|
+
if (!block(0)) return false;
|
|
236
|
+
} else if (position < tokens.length && !take(";")) return false;
|
|
237
|
+
}
|
|
238
|
+
return rules > 0;
|
|
239
|
+
}
|
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
/** Closed admission grammar, not an evaluator. Unknown syntax never acquires a proof. */
|
|
2
2
|
export function isPassiveJqFilter(filter: string): boolean {
|
|
3
|
+
return admitFilter(filter, 0, new Set());
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function admitFilter(
|
|
7
|
+
filter: string,
|
|
8
|
+
interpolationDepth: number,
|
|
9
|
+
inherited: ReadonlySet<string>,
|
|
10
|
+
): boolean {
|
|
11
|
+
const bindings = new Set(inherited);
|
|
12
|
+
if (interpolationDepth > 32) return false;
|
|
3
13
|
if (filter.length > 65_536) return false;
|
|
4
14
|
if (new TextEncoder().encode(filter).byteLength > 65_536) return false;
|
|
5
15
|
const tokens: string[] = [];
|
|
6
16
|
const lexer =
|
|
7
|
-
/\s+|"(?:[^"\\\x00-\x1f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"|(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?|[A-Za-z_][A-Za-z0-9_]
|
|
17
|
+
/\s+|"(?:[^"\\\x00-\x1f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"|(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?|[$@]?[A-Za-z_][A-Za-z0-9_]*|\.[A-Za-z_][A-Za-z0-9_]*|\.\.|\/\/=|[|+*/%-]=|\/\/|==|!=|<=|>=|=|[.{}[\]():,;?|+*/%<>-]/y; // eslint-disable-line eslint/no-control-regex -- JSON strings exclude literal control bytes.
|
|
8
18
|
let offset = 0;
|
|
9
19
|
while (offset < filter.length) {
|
|
20
|
+
if (filter[offset] === '"') {
|
|
21
|
+
const quoted = readString(filter, offset, 0);
|
|
22
|
+
if (!quoted) return false;
|
|
23
|
+
tokens.push(filter.slice(offset, quoted.end));
|
|
24
|
+
if (tokens.length > 4096) return false;
|
|
25
|
+
offset = quoted.end;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
10
28
|
lexer.lastIndex = offset;
|
|
11
29
|
const match = lexer.exec(filter);
|
|
12
30
|
if (!match) return false;
|
|
@@ -22,7 +40,18 @@ export function isPassiveJqFilter(filter: string): boolean {
|
|
|
22
40
|
};
|
|
23
41
|
const identifier = (token: string | undefined): boolean =>
|
|
24
42
|
/^[A-Za-z_][A-Za-z0-9_]*$/.test(token ?? "");
|
|
25
|
-
const string = (token: string | undefined): boolean =>
|
|
43
|
+
const string = (token: string | undefined): boolean => {
|
|
44
|
+
if (!token?.startsWith('"')) return false;
|
|
45
|
+
const quoted = readString(token, 0, 0);
|
|
46
|
+
return (
|
|
47
|
+
quoted !== undefined &&
|
|
48
|
+
quoted.interpolations.every(([start, end]) =>
|
|
49
|
+
admitFilter(token.slice(start, end), interpolationDepth + 1, bindings),
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
const variable = (token: string | undefined): token is string =>
|
|
54
|
+
/^\$[A-Za-z_][A-Za-z0-9_]*$/.test(token ?? "") && token !== "$ENV";
|
|
26
55
|
const number = (token: string | undefined): boolean => /^[0-9]/.test(token ?? "");
|
|
27
56
|
const noArgs = new Set([
|
|
28
57
|
"length",
|
|
@@ -30,6 +59,16 @@ export function isPassiveJqFilter(filter: string): boolean {
|
|
|
30
59
|
"keys_unsorted",
|
|
31
60
|
"type",
|
|
32
61
|
"values",
|
|
62
|
+
"objects",
|
|
63
|
+
"arrays",
|
|
64
|
+
"strings",
|
|
65
|
+
"numbers",
|
|
66
|
+
"booleans",
|
|
67
|
+
"nulls",
|
|
68
|
+
"scalars",
|
|
69
|
+
"iterables",
|
|
70
|
+
"first",
|
|
71
|
+
"last",
|
|
33
72
|
"empty",
|
|
34
73
|
"add",
|
|
35
74
|
"min",
|
|
@@ -41,6 +80,26 @@ export function isPassiveJqFilter(filter: string): boolean {
|
|
|
41
80
|
"reverse",
|
|
42
81
|
"to_entries",
|
|
43
82
|
"from_entries",
|
|
83
|
+
"tostring",
|
|
84
|
+
"tonumber",
|
|
85
|
+
"tojson",
|
|
86
|
+
"fromjson",
|
|
87
|
+
"ascii_downcase",
|
|
88
|
+
"ascii_upcase",
|
|
89
|
+
"floor",
|
|
90
|
+
"ceil",
|
|
91
|
+
"round",
|
|
92
|
+
"abs",
|
|
93
|
+
"utf8bytelength",
|
|
94
|
+
"@text",
|
|
95
|
+
"@json",
|
|
96
|
+
"@html",
|
|
97
|
+
"@uri",
|
|
98
|
+
"@csv",
|
|
99
|
+
"@tsv",
|
|
100
|
+
"@sh",
|
|
101
|
+
"@base64",
|
|
102
|
+
"@base64d",
|
|
44
103
|
]);
|
|
45
104
|
const oneArg = new Set([
|
|
46
105
|
"map",
|
|
@@ -54,8 +113,37 @@ export function isPassiveJqFilter(filter: string): boolean {
|
|
|
54
113
|
"any",
|
|
55
114
|
"all",
|
|
56
115
|
"join",
|
|
116
|
+
"split",
|
|
117
|
+
"startswith",
|
|
118
|
+
"endswith",
|
|
119
|
+
"ltrimstr",
|
|
120
|
+
"rtrimstr",
|
|
121
|
+
"test",
|
|
122
|
+
"index",
|
|
123
|
+
"rindex",
|
|
124
|
+
"indices",
|
|
125
|
+
"min_by",
|
|
126
|
+
"max_by",
|
|
127
|
+
"del",
|
|
128
|
+
"with_entries",
|
|
129
|
+
"getpath",
|
|
130
|
+
]);
|
|
131
|
+
const multipleArgs = new Map<string, readonly number[]>([
|
|
132
|
+
["test", [1, 2]],
|
|
133
|
+
["match", [1, 2]],
|
|
134
|
+
["capture", [1, 2]],
|
|
135
|
+
["sub", [2, 3]],
|
|
136
|
+
["gsub", [2, 3]],
|
|
57
137
|
]);
|
|
58
138
|
const operators = new Set([
|
|
139
|
+
"=",
|
|
140
|
+
"|=",
|
|
141
|
+
"+=",
|
|
142
|
+
"-=",
|
|
143
|
+
"*=",
|
|
144
|
+
"/=",
|
|
145
|
+
"%=",
|
|
146
|
+
"//=",
|
|
59
147
|
"|",
|
|
60
148
|
"//",
|
|
61
149
|
"==",
|
|
@@ -74,18 +162,32 @@ export function isPassiveJqFilter(filter: string): boolean {
|
|
|
74
162
|
]);
|
|
75
163
|
const expression = (depth: number, commas = false): boolean => {
|
|
76
164
|
if (depth > 64 || !term(depth)) return false;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
165
|
+
while (true) {
|
|
166
|
+
if (take("as")) {
|
|
167
|
+
const name = tokens[position++];
|
|
168
|
+
if (!variable(name) || !take("|")) return false;
|
|
169
|
+
bindings.add(name);
|
|
170
|
+
} else if (operators.has(tokens[position] ?? "") || (commas && tokens[position] === ",")) {
|
|
171
|
+
position++;
|
|
172
|
+
} else break;
|
|
81
173
|
if (!term(depth)) return false;
|
|
82
174
|
}
|
|
83
175
|
return true;
|
|
84
176
|
};
|
|
177
|
+
const field = (): boolean => /^\.[A-Za-z_]/.test(tokens[position] ?? "");
|
|
85
178
|
const term = (depth: number): boolean => {
|
|
86
179
|
if (take("-")) return number(tokens[position]) && (position++, true);
|
|
87
|
-
if (take("
|
|
88
|
-
|
|
180
|
+
if (take("if")) {
|
|
181
|
+
do {
|
|
182
|
+
if (!expression(depth + 1) || !take("then") || !expression(depth + 1)) return false;
|
|
183
|
+
} while (take("elif"));
|
|
184
|
+
if (!take("else") || !expression(depth + 1) || !take("end")) return false;
|
|
185
|
+
} else if (field()) {
|
|
186
|
+
position++;
|
|
187
|
+
} else if (take("..")) {
|
|
188
|
+
// Recursive descent only selects values from the current input.
|
|
189
|
+
} else if (take(".")) {
|
|
190
|
+
if (string(tokens[position])) position++;
|
|
89
191
|
} else if (take("(")) {
|
|
90
192
|
if (!expression(depth + 1, true) || !take(")")) return false;
|
|
91
193
|
} else if (take("[")) {
|
|
@@ -93,37 +195,56 @@ export function isPassiveJqFilter(filter: string): boolean {
|
|
|
93
195
|
} else if (take("{")) {
|
|
94
196
|
if (!take("}")) {
|
|
95
197
|
do {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
198
|
+
if (take("(")) {
|
|
199
|
+
if (!expression(depth + 1) || !take(")") || !take(":") || !expression(depth + 1))
|
|
200
|
+
return false;
|
|
201
|
+
} else {
|
|
202
|
+
const key = tokens[position];
|
|
203
|
+
if (!identifier(key) && !string(key)) return false;
|
|
204
|
+
position++;
|
|
205
|
+
if (take(":")) {
|
|
206
|
+
if (!expression(depth + 1)) return false;
|
|
207
|
+
} else if (!identifier(key)) return false;
|
|
208
|
+
}
|
|
102
209
|
} while (take(","));
|
|
103
210
|
if (!take("}")) return false;
|
|
104
211
|
}
|
|
105
212
|
} else {
|
|
106
213
|
const token = tokens[position];
|
|
107
|
-
if (
|
|
214
|
+
if (
|
|
215
|
+
(variable(token) && bindings.has(token)) ||
|
|
216
|
+
string(token) ||
|
|
217
|
+
number(token) ||
|
|
218
|
+
["true", "false", "null"].includes(token ?? "")
|
|
219
|
+
)
|
|
108
220
|
position++;
|
|
109
|
-
else if (noArgs.has(token ?? ""))
|
|
110
|
-
else if (oneArg.has(token ?? "")) {
|
|
221
|
+
else if (noArgs.has(token ?? "")) {
|
|
111
222
|
position++;
|
|
112
|
-
if (
|
|
223
|
+
if (token?.startsWith("@") && string(tokens[position])) position++;
|
|
224
|
+
} else if (oneArg.has(token ?? "") || multipleArgs.has(token ?? "")) {
|
|
225
|
+
position++;
|
|
226
|
+
if (!take("(") || !expression(depth + 1)) return false;
|
|
227
|
+
let count = 1;
|
|
228
|
+
while (take(";")) {
|
|
229
|
+
count++;
|
|
230
|
+
if (!expression(depth + 1)) return false;
|
|
231
|
+
}
|
|
232
|
+
if (!(multipleArgs.get(token ?? "") ?? [1]).includes(count) || !take(")")) return false;
|
|
113
233
|
} else return false;
|
|
114
234
|
}
|
|
115
235
|
while (true) {
|
|
116
236
|
if (take("?")) continue;
|
|
237
|
+
if (field()) {
|
|
238
|
+
position++;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
117
241
|
if (take(".")) {
|
|
118
|
-
if (!
|
|
242
|
+
if (!string(tokens[position])) return false;
|
|
119
243
|
position++;
|
|
120
244
|
} else if (take("[")) {
|
|
121
245
|
if (!take("]")) {
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
position++;
|
|
125
|
-
} else if (string(tokens[position]) || number(tokens[position])) position++;
|
|
126
|
-
else return false;
|
|
246
|
+
if (tokens[position] !== ":" && !expression(depth + 1, true)) return false;
|
|
247
|
+
if (take(":") && tokens[position] !== "]" && !expression(depth + 1)) return false;
|
|
127
248
|
if (!take("]")) return false;
|
|
128
249
|
}
|
|
129
250
|
} else break;
|
|
@@ -133,6 +254,44 @@ export function isPassiveJqFilter(filter: string): boolean {
|
|
|
133
254
|
return expression(0, true) && position === tokens.length;
|
|
134
255
|
}
|
|
135
256
|
|
|
257
|
+
function readString(
|
|
258
|
+
text: string,
|
|
259
|
+
start: number,
|
|
260
|
+
depth: number,
|
|
261
|
+
): { end: number; interpolations: [number, number][] } | undefined {
|
|
262
|
+
if (depth > 32) return undefined;
|
|
263
|
+
const interpolations: [number, number][] = [];
|
|
264
|
+
let offset = start + 1;
|
|
265
|
+
while (offset < text.length) {
|
|
266
|
+
const character = text[offset++];
|
|
267
|
+
if (character === '"') return { end: offset, interpolations };
|
|
268
|
+
if (!character || character.charCodeAt(0) < 32) return undefined;
|
|
269
|
+
if (character !== "\\") continue;
|
|
270
|
+
const escape = text[offset++];
|
|
271
|
+
if (escape === "(") {
|
|
272
|
+
const expressionStart = offset;
|
|
273
|
+
let parentheses = 1;
|
|
274
|
+
while (offset < text.length && parentheses > 0) {
|
|
275
|
+
if (text[offset] === '"') {
|
|
276
|
+
const nested = readString(text, offset, depth + 1);
|
|
277
|
+
if (!nested) return undefined;
|
|
278
|
+
offset = nested.end;
|
|
279
|
+
} else {
|
|
280
|
+
const token = text[offset++];
|
|
281
|
+
if (token === "(") parentheses++;
|
|
282
|
+
if (token === ")") parentheses--;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (parentheses !== 0) return undefined;
|
|
286
|
+
interpolations.push([expressionStart, offset - 1]);
|
|
287
|
+
} else if (escape === "u") {
|
|
288
|
+
if (!/^[0-9a-fA-F]{4}$/.test(text.slice(offset, offset + 4))) return undefined;
|
|
289
|
+
offset += 4;
|
|
290
|
+
} else if (!escape || !'"\\/bfnrt'.includes(escape)) return undefined;
|
|
291
|
+
}
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
136
295
|
/** Only known formatting/input flags and one literal program, followed by file operands. */
|
|
137
296
|
export function passiveJqProgramIndex(argv: string[]): number | undefined {
|
|
138
297
|
if (argv[0]?.split("/").at(-1) !== "jq") return undefined;
|