redac---n 0.0.0-stage → 0.0.2
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/LICENSE +21 -0
- package/README.md +323 -2
- package/THIRD_PARTY_NOTICES.md +59 -0
- package/dist/aho.d.ts +25 -0
- package/dist/aho.js +116 -0
- package/dist/base64.d.ts +10 -0
- package/dist/base64.js +29 -0
- package/dist/bip39-data.d.ts +2 -0
- package/dist/bip39-data.js +2 -0
- package/dist/cache.d.ts +14 -0
- package/dist/cache.js +40 -0
- package/dist/calls.d.ts +64 -0
- package/dist/calls.js +269 -0
- package/dist/charlm-data.d.ts +2 -0
- package/dist/charlm-data.js +3 -0
- package/dist/charlm.d.ts +7 -0
- package/dist/charlm.js +65 -0
- package/dist/checksums.d.ts +10 -0
- package/dist/checksums.js +98 -0
- package/dist/commands.d.ts +72 -0
- package/dist/commands.js +787 -0
- package/dist/context.d.ts +98 -0
- package/dist/context.js +469 -0
- package/dist/encoded.d.ts +11 -0
- package/dist/encoded.js +336 -0
- package/dist/environment.d.ts +10 -0
- package/dist/environment.js +62 -0
- package/dist/formats-data.d.ts +2 -0
- package/dist/formats-data.js +405 -0
- package/dist/formats.d.ts +5 -0
- package/dist/formats.js +470 -0
- package/dist/hmac.d.ts +3 -0
- package/dist/hmac.js +87 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +488 -0
- package/dist/json.d.ts +82 -0
- package/dist/json.js +392 -0
- package/dist/known.d.ts +34 -0
- package/dist/known.js +340 -0
- package/dist/lines.d.ts +28 -0
- package/dist/lines.js +69 -0
- package/dist/lookup.d.ts +5 -0
- package/dist/lookup.js +7 -0
- package/dist/model-data.d.ts +3 -0
- package/dist/model-data.js +4 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +19 -0
- package/dist/named.d.ts +20 -0
- package/dist/named.js +3257 -0
- package/dist/names.d.ts +106 -0
- package/dist/names.js +1517 -0
- package/dist/normalize.d.ts +25 -0
- package/dist/normalize.js +529 -0
- package/dist/pem.d.ts +32 -0
- package/dist/pem.js +269 -0
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +470 -0
- package/dist/public.d.ts +44 -0
- package/dist/public.js +362 -0
- package/dist/publickeys.d.ts +41 -0
- package/dist/publickeys.js +232 -0
- package/dist/random.d.ts +29 -0
- package/dist/random.js +165 -0
- package/dist/repeats.d.ts +21 -0
- package/dist/repeats.js +80 -0
- package/dist/rules.d.ts +42 -0
- package/dist/rules.js +58 -0
- package/dist/runs.d.ts +10 -0
- package/dist/runs.js +36 -0
- package/dist/secretfile.d.ts +52 -0
- package/dist/secretfile.js +722 -0
- package/dist/shell.d.ts +127 -0
- package/dist/shell.js +703 -0
- package/dist/spans.d.ts +61 -0
- package/dist/spans.js +287 -0
- package/dist/sql.d.ts +20 -0
- package/dist/sql.js +66 -0
- package/dist/stream.d.ts +46 -0
- package/dist/stream.js +150 -0
- package/dist/structure.d.ts +24 -0
- package/dist/structure.js +1625 -0
- package/dist/syntax.d.ts +156 -0
- package/dist/syntax.js +446 -0
- package/dist/transcript.d.ts +30 -0
- package/dist/transcript.js +713 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.js +1 -0
- package/dist/unnamed.d.ts +59 -0
- package/dist/unnamed.js +939 -0
- package/dist/urls.d.ts +70 -0
- package/dist/urls.js +303 -0
- package/dist/values.d.ts +178 -0
- package/dist/values.js +1621 -0
- package/dist/yaml.d.ts +40 -0
- package/dist/yaml.js +174 -0
- package/docs/creddata.svg +318 -0
- package/docs/guessing.md +139 -0
- package/docs/holdout.svg +85 -0
- package/docs/layers.md +80 -0
- package/docs/models.svg +81 -0
- package/docs/real-text.svg +112 -0
- package/package.json +57 -5
package/dist/json.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON inside a text: string literals decoded and mapped back (tool calls, serialized requests, logs of JSON), whole
|
|
3
|
+
* documents parsed with where their values are (API responses, `kubectl … -o json`), and the object around a position
|
|
4
|
+
* (a JSON Web Key's members).
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The keys of agents' tool inputs, one vocabulary for the readers of a serialized tool call (here) and of a parsed one
|
|
8
|
+
* (transcript.ts): the shell command a tool runs, the path it reads or writes, and the text it writes there.
|
|
9
|
+
*/
|
|
10
|
+
export declare const TOOL_COMMAND_KEYS: string[];
|
|
11
|
+
export declare const TOOL_PATH_KEYS: string[];
|
|
12
|
+
export declare const TOOL_CONTENT_KEYS: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Whether a text may hold JSON string literals worth decoding: escapes a document's literals carry (a line break,
|
|
15
|
+
* a tab, a quote, a slash, a Unicode escape), or a tool's text argument, decoded even without escapes (a one-line
|
|
16
|
+
* Edit's `"new_string":" password: a b c"`). The same escapes jsonStringLiterals decodes.
|
|
17
|
+
*/
|
|
18
|
+
export declare function mayHoldJsonText(text: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The single-quoted literals of a Python repr (`{'stdout': 'PGPASSWORD=…\n…', 'returncode': 0}`, a subprocess
|
|
21
|
+
* result printed whole) that are documents of their own, as jsonStringLiterals reads them: decoded and mapped as
|
|
22
|
+
* those are.
|
|
23
|
+
*/
|
|
24
|
+
export declare function pythonReprLiterals(text: string): Generator<{
|
|
25
|
+
decoded: string;
|
|
26
|
+
map: number[];
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* JSON string literals worth scanning as the text they encode, decoded, each with a map from decoded positions to
|
|
30
|
+
* positions in the text (one entry per decoded character, plus one for the end): documents (several lines, a tab's
|
|
31
|
+
* columns, quoted text inside), and a single config line under a tool's text key or a command line under a shell's,
|
|
32
|
+
* escapes or not.
|
|
33
|
+
*/
|
|
34
|
+
export declare function jsonStringLiterals(text: string): {
|
|
35
|
+
decoded: string;
|
|
36
|
+
map: number[];
|
|
37
|
+
}[];
|
|
38
|
+
/**
|
|
39
|
+
* Where each JSON string literal's content is, between its quotes: what no finding's span should cross. Only the
|
|
40
|
+
* literals JSON's syntax stands around (isJsonPlaced): a `"` inside another language's single-quoted string
|
|
41
|
+
* (`-p'Kx"9…' -e "SELECT 1"`) opens no JSON literal.
|
|
42
|
+
*/
|
|
43
|
+
export declare function jsonLiteralSpans(text: string): [start: number, end: number][];
|
|
44
|
+
/** A JSON value parsed with where its scalars are in the source: a string's span is its content, quotes excluded. */
|
|
45
|
+
export type JsonNode = {
|
|
46
|
+
kind: "object";
|
|
47
|
+
entries: [key: string, value: JsonNode][];
|
|
48
|
+
} | {
|
|
49
|
+
kind: "array";
|
|
50
|
+
items: JsonNode[];
|
|
51
|
+
} | {
|
|
52
|
+
kind: "string";
|
|
53
|
+
value: string;
|
|
54
|
+
start: number;
|
|
55
|
+
end: number;
|
|
56
|
+
} | {
|
|
57
|
+
kind: "literal";
|
|
58
|
+
value: number | boolean | null;
|
|
59
|
+
start: number;
|
|
60
|
+
end: number;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* The JSON value starting at `at` (after blanks), with each scalar's source span, and where the value ends; undefined
|
|
64
|
+
* if the text there isn't JSON, and then `progress.reached` says how far it was read. Nesting deeper than `maxDepth`
|
|
65
|
+
* counts as not JSON.
|
|
66
|
+
*/
|
|
67
|
+
export declare function parseJson(text: string, at: number, maxDepth?: number, progress?: {
|
|
68
|
+
reached: number;
|
|
69
|
+
}): {
|
|
70
|
+
node: JsonNode;
|
|
71
|
+
end: number;
|
|
72
|
+
} | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* The JSON documents in a text (API responses, `kubectl … -o json`, `terraform output -json`): values that start a
|
|
75
|
+
* line. A parsed document's nested objects aren't tried again. (What the starts that aren't JSON read is bounded, four
|
|
76
|
+
* times the text, so the scan stays linear: a log's `[INFO] …` lines fail at once, and any number of them is tried.
|
|
77
|
+
* Past the bound, a start inside what a failed one read is skipped, and the ones after it are tried: `[\n` repeated
|
|
78
|
+
* hides no document after it.)
|
|
79
|
+
*/
|
|
80
|
+
export declare function jsonDocuments(text: string): Generator<JsonNode>;
|
|
81
|
+
/** Whether [start, end) is a member of a JSON Web Key: its innermost object has a `kty` of its own (JSON, or a JS or Python literal). */
|
|
82
|
+
export declare function inJwk(text: string, start: number, end: number): boolean;
|
package/dist/json.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON inside a text: string literals decoded and mapped back (tool calls, serialized requests, logs of JSON), whole
|
|
3
|
+
* documents parsed with where their values are (API responses, `kubectl … -o json`), and the object around a position
|
|
4
|
+
* (a JSON Web Key's members).
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The keys of agents' tool inputs, one vocabulary for the readers of a serialized tool call (here) and of a parsed one
|
|
8
|
+
* (transcript.ts): the shell command a tool runs, the path it reads or writes, and the text it writes there.
|
|
9
|
+
*/
|
|
10
|
+
export const TOOL_COMMAND_KEYS = ["command", "cmd", "script", "shell_command", "run"];
|
|
11
|
+
export const TOOL_PATH_KEYS = ["file_path", "path", "filePath", "filename", "file", "target_file", "notebook_path"];
|
|
12
|
+
export const TOOL_CONTENT_KEYS = [
|
|
13
|
+
"content",
|
|
14
|
+
"contents",
|
|
15
|
+
"file_text",
|
|
16
|
+
"new_string",
|
|
17
|
+
"new_str",
|
|
18
|
+
"old_string",
|
|
19
|
+
"old_str",
|
|
20
|
+
"old_text",
|
|
21
|
+
"new_text",
|
|
22
|
+
"search",
|
|
23
|
+
"replace",
|
|
24
|
+
"insert_text",
|
|
25
|
+
"new_source",
|
|
26
|
+
"text",
|
|
27
|
+
"code",
|
|
28
|
+
"edits",
|
|
29
|
+
];
|
|
30
|
+
/** The keys tools take text under: an editor's `new_string`, a writer's `content`, a shell's `command`, a tool's `input`. */
|
|
31
|
+
const TOOL_TEXT_KEY = String.raw `(?:${[...TOOL_CONTENT_KEYS, ...TOOL_COMMAND_KEYS, "input"].join("|")})`;
|
|
32
|
+
const TOOL_TEXT_ARGUMENT = new RegExp(String.raw `"${TOOL_TEXT_KEY}"[ \t]*:[ \t]*"`);
|
|
33
|
+
/** A literal's opening quote right after a tool's text key (`"new_string": "`, escaped any number of times). */
|
|
34
|
+
const AFTER_TOOL_TEXT_KEY = new RegExp(String.raw `(?<![\w$-])${TOOL_TEXT_KEY}\\*"[ \t]*:[ \t]*$`);
|
|
35
|
+
/** A literal's opening quote right after a shell command's key (`"command": "`, `"cmd": "`): a command line, read as one. */
|
|
36
|
+
const AFTER_COMMAND_KEY = new RegExp(String.raw `(?<![\w$-])(?:${TOOL_COMMAND_KEYS.join("|")})\\*"[ \t]*:[ \t]*$`);
|
|
37
|
+
/**
|
|
38
|
+
* Whether a text may hold JSON string literals worth decoding: escapes a document's literals carry (a line break,
|
|
39
|
+
* a tab, a quote, a slash, a Unicode escape), or a tool's text argument, decoded even without escapes (a one-line
|
|
40
|
+
* Edit's `"new_string":" password: a b c"`). The same escapes jsonStringLiterals decodes.
|
|
41
|
+
*/
|
|
42
|
+
export function mayHoldJsonText(text) {
|
|
43
|
+
return /\\[nrtbf"/u\\]/.test(text) || TOOL_TEXT_ARGUMENT.test(text);
|
|
44
|
+
}
|
|
45
|
+
/** The longest string literal decoded: 4 MB, past any tool result an agent reads whole. */
|
|
46
|
+
const MAX_LITERAL = 4_000_000;
|
|
47
|
+
/** JSON's one-character escapes, by the character after the backslash. */
|
|
48
|
+
const JSON_ESCAPES = {
|
|
49
|
+
n: "\n",
|
|
50
|
+
t: "\t",
|
|
51
|
+
r: "\r",
|
|
52
|
+
'"': '"',
|
|
53
|
+
"\\": "\\",
|
|
54
|
+
"/": "/",
|
|
55
|
+
b: "\b",
|
|
56
|
+
f: "\f",
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The JSON escape whose backslash is at `at`: what it decodes to and how many characters it takes; undefined for one
|
|
60
|
+
* JSON doesn't have (`\x41`, `\'`), which a lenient reader keeps as written and a strict one rejects.
|
|
61
|
+
*/
|
|
62
|
+
function jsonEscape(text, at) {
|
|
63
|
+
const simple = JSON_ESCAPES[text[at + 1]];
|
|
64
|
+
if (simple !== undefined)
|
|
65
|
+
return [simple, 2];
|
|
66
|
+
if (text[at + 1] === "u" && /^[0-9a-fA-F]{4}$/.test(text.slice(at + 2, at + 6)))
|
|
67
|
+
return [String.fromCharCode(parseInt(text.slice(at + 2, at + 6), 16)), 6];
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Each JSON string literal of a text that closes on its line: its content decoded, a map from decoded positions to
|
|
72
|
+
* positions in the text (one per decoded character, plus one for the end), its escapes, and its opening quote. With
|
|
73
|
+
* `quote` `'`, a Python repr's literals, whose escapes are JSON's and `\'`, `\xHH`.
|
|
74
|
+
*/
|
|
75
|
+
function* stringLiterals(text, quote = '"') {
|
|
76
|
+
const escape = quote === "'" ? pythonEscape : jsonEscape;
|
|
77
|
+
for (let i = text.indexOf(quote); i >= 0; i = text.indexOf(quote, i + 1)) {
|
|
78
|
+
let j = i + 1;
|
|
79
|
+
let decoded = "";
|
|
80
|
+
const map = [];
|
|
81
|
+
let escapes = 0;
|
|
82
|
+
let ok = false;
|
|
83
|
+
while (j < text.length) {
|
|
84
|
+
const c = text[j];
|
|
85
|
+
if (c === quote) {
|
|
86
|
+
ok = true;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
// (A literal ends at a raw line break, which JSON doesn't allow in one, or past MAX_LITERAL.)
|
|
90
|
+
if (c === "\n" || j - i > MAX_LITERAL)
|
|
91
|
+
break;
|
|
92
|
+
if (c === "\\") {
|
|
93
|
+
const decodedEscape = escape(text, j);
|
|
94
|
+
if (decodedEscape) {
|
|
95
|
+
map.push(j);
|
|
96
|
+
decoded += decodedEscape[0];
|
|
97
|
+
j += decodedEscape[1];
|
|
98
|
+
escapes++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const n = text[j + 1];
|
|
102
|
+
// An escape JSON doesn't have (`\x41`, `\'`, `\0`: a source file's string) stays as written, and the
|
|
103
|
+
// literal goes on: its closing quote is still the one that closes it.
|
|
104
|
+
if (n === undefined || n === "\n")
|
|
105
|
+
break;
|
|
106
|
+
map.push(j, j + 1);
|
|
107
|
+
decoded += c + n;
|
|
108
|
+
j += 2;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
map.push(j);
|
|
112
|
+
decoded += c;
|
|
113
|
+
j++;
|
|
114
|
+
}
|
|
115
|
+
// A literal that doesn't close resumes after where it stopped: the quotes inside it were escaped.
|
|
116
|
+
if (!ok) {
|
|
117
|
+
i = j;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
map.push(j);
|
|
121
|
+
yield { decoded, map, escapes, open: i };
|
|
122
|
+
i = j;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** A Python string's escape at `at`: JSON's, and `\'` and `\xHH`, as `repr()` writes them. */
|
|
126
|
+
function pythonEscape(text, at) {
|
|
127
|
+
if (text[at + 1] === "'")
|
|
128
|
+
return ["'", 2];
|
|
129
|
+
if (text[at + 1] === "x" && /^[0-9a-fA-F]{2}$/.test(text.slice(at + 2, at + 4)))
|
|
130
|
+
return [String.fromCharCode(parseInt(text.slice(at + 2, at + 4), 16)), 4];
|
|
131
|
+
return jsonEscape(text, at);
|
|
132
|
+
}
|
|
133
|
+
/** Whether a decoded literal is a document of its own: several lines, or a tab's columns. */
|
|
134
|
+
function isOutputText(decoded) {
|
|
135
|
+
return /\n.*\S/.test(decoded) || decoded.includes("\t");
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The single-quoted literals of a Python repr (`{'stdout': 'PGPASSWORD=…\n…', 'returncode': 0}`, a subprocess
|
|
139
|
+
* result printed whole) that are documents of their own, as jsonStringLiterals reads them: decoded and mapped as
|
|
140
|
+
* those are.
|
|
141
|
+
*/
|
|
142
|
+
export function* pythonReprLiterals(text) {
|
|
143
|
+
if (!/['\]][ \t]*:[ \t]*'|\('|, '/.test(text) || !/\\[nt]/.test(text))
|
|
144
|
+
return;
|
|
145
|
+
for (const { decoded, map, escapes } of stringLiterals(text, "'"))
|
|
146
|
+
if (escapes > 0 && isOutputText(decoded))
|
|
147
|
+
yield { decoded, map };
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* JSON string literals worth scanning as the text they encode, decoded, each with a map from decoded positions to
|
|
151
|
+
* positions in the text (one entry per decoded character, plus one for the end): documents (several lines, a tab's
|
|
152
|
+
* columns, quoted text inside), and a single config line under a tool's text key or a command line under a shell's,
|
|
153
|
+
* escapes or not.
|
|
154
|
+
*/
|
|
155
|
+
export function jsonStringLiterals(text) {
|
|
156
|
+
const out = [];
|
|
157
|
+
for (const { decoded, map, escapes, open } of stringLiterals(text)) {
|
|
158
|
+
// Only literals that are documents themselves: several lines, a tab's columns, or quoted text inside. A code string
|
|
159
|
+
// ending in one `\n` (a line of a C or Python array) is judged in place, with its neighbours. (Terminal output's
|
|
160
|
+
// colour codes, `\u001b[32m`, normalize.ts reads where they are written.)
|
|
161
|
+
// Also a single config line quoted on its own (an Edit tool's `old_string`): ` private_key_passphrase: a b c`.
|
|
162
|
+
// (Only under a tool's input key: an i18n file's `"pwd_hint": "Password: 8-64 characters"` is text.)
|
|
163
|
+
const configLine = decoded.length >= 12 &&
|
|
164
|
+
/^[ \t]*[\w.-]{1,80}[ \t]*[:=][ \t]*\S[^\n]*\s/.test(decoded) &&
|
|
165
|
+
/pass|secret|token|key|cred|auth|pwd/i.test(decoded) &&
|
|
166
|
+
AFTER_TOOL_TEXT_KEY.test(text.slice(Math.max(0, open - 40), open));
|
|
167
|
+
// (And a command line under a shell's key, `{"command": "mysql -u root -pV shop"}`: read as the command it is, with
|
|
168
|
+
// or without flags, `{"command": "argocd-k8s-auth"}`.)
|
|
169
|
+
const commandLine = decoded.length >= 8 && AFTER_COMMAND_KEY.test(text.slice(Math.max(0, open - 40), open));
|
|
170
|
+
if (configLine || commandLine)
|
|
171
|
+
out.push({ decoded, map });
|
|
172
|
+
else if (escapes > 0 &&
|
|
173
|
+
decoded.length >= 20 &&
|
|
174
|
+
(isOutputText(decoded) ||
|
|
175
|
+
decoded.includes('"') ||
|
|
176
|
+
// (An escaped slash, PHP's `"https:\/\/…"`, only where JSON's syntax stands around the literal: in a sed
|
|
177
|
+
// script in a shell's string, `"s/^DB_PASSWORD=.*/DB_PASSWORD=a\/b/"`, it escapes the script's delimiter.)
|
|
178
|
+
(decoded.includes("/") &&
|
|
179
|
+
/\\\//.test(text.slice(map[0], map[map.length - 1])) &&
|
|
180
|
+
isJsonPlaced(text, open, map[map.length - 1]))))
|
|
181
|
+
out.push({ decoded, map });
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
/** The most characters before a literal's quote looked at for what opens it. */
|
|
186
|
+
const OPENER_WINDOW = 40;
|
|
187
|
+
/** Whether the 40 characters before `at` end in `{`, `[`, `,` or `:` and blanks, or are all blanks. */
|
|
188
|
+
function followsJsonOpener(text, at) {
|
|
189
|
+
const from = Math.max(0, at - OPENER_WINDOW);
|
|
190
|
+
let i = at;
|
|
191
|
+
while (i > from && (text[i - 1] === " " || text[i - 1] === "\t" || text[i - 1] === "\r" || text[i - 1] === "\n"))
|
|
192
|
+
i--;
|
|
193
|
+
return i === from || "{[,:".includes(text[i - 1]);
|
|
194
|
+
}
|
|
195
|
+
/** Whether JSON's syntax stands around the literal whose quotes are at `open` and `close`: `{`, `[`, `,` or `:` before; `:`, `,`, `}` or `]` after, or the text's ends. */
|
|
196
|
+
function isJsonPlaced(text, open, close) {
|
|
197
|
+
return followsJsonOpener(text, open) && /^[ \t\r\n]*(?:[:,}\]]|$)/.test(text.slice(close + 1, close + 41));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Where each JSON string literal's content is, between its quotes: what no finding's span should cross. Only the
|
|
201
|
+
* literals JSON's syntax stands around (isJsonPlaced): a `"` inside another language's single-quoted string
|
|
202
|
+
* (`-p'Kx"9…' -e "SELECT 1"`) opens no JSON literal.
|
|
203
|
+
*/
|
|
204
|
+
export function jsonLiteralSpans(text) {
|
|
205
|
+
const out = [];
|
|
206
|
+
for (const { map, open } of stringLiterals(text)) {
|
|
207
|
+
const close = map[map.length - 1];
|
|
208
|
+
if (isJsonPlaced(text, open, close))
|
|
209
|
+
out.push([open + 1, close]);
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
/** A JSON literal as JSON's grammar has it (no leading zeros: `02` in `[02:39]` isn't one), read where it starts. */
|
|
214
|
+
const LITERAL = /(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][-+]?\d+)?)(?![\w.])/y;
|
|
215
|
+
const KEYWORDS = { true: true, false: false, null: null };
|
|
216
|
+
/**
|
|
217
|
+
* The JSON value starting at `at` (after blanks), with each scalar's source span, and where the value ends; undefined
|
|
218
|
+
* if the text there isn't JSON, and then `progress.reached` says how far it was read. Nesting deeper than `maxDepth`
|
|
219
|
+
* counts as not JSON.
|
|
220
|
+
*/
|
|
221
|
+
export function parseJson(text, at, maxDepth = 100, progress) {
|
|
222
|
+
let i = at;
|
|
223
|
+
const blanks = () => {
|
|
224
|
+
while (i < text.length && (text[i] === " " || text[i] === "\t" || text[i] === "\n" || text[i] === "\r"))
|
|
225
|
+
i++;
|
|
226
|
+
};
|
|
227
|
+
const string = () => {
|
|
228
|
+
const start = ++i;
|
|
229
|
+
let value = "";
|
|
230
|
+
while (i < text.length && text[i] !== '"') {
|
|
231
|
+
const c = text[i];
|
|
232
|
+
if (c === "\n")
|
|
233
|
+
return undefined;
|
|
234
|
+
if (c !== "\\") {
|
|
235
|
+
value += c;
|
|
236
|
+
i++;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const escape = jsonEscape(text, i);
|
|
240
|
+
if (!escape)
|
|
241
|
+
return undefined;
|
|
242
|
+
value += escape[0];
|
|
243
|
+
i += escape[1];
|
|
244
|
+
}
|
|
245
|
+
if (i >= text.length)
|
|
246
|
+
return undefined;
|
|
247
|
+
return { kind: "string", value, start, end: i++ };
|
|
248
|
+
};
|
|
249
|
+
const value = (depth) => {
|
|
250
|
+
if (depth > maxDepth)
|
|
251
|
+
return undefined;
|
|
252
|
+
blanks();
|
|
253
|
+
const c = text[i];
|
|
254
|
+
if (c === '"')
|
|
255
|
+
return string();
|
|
256
|
+
if (c === "{" || c === "[") {
|
|
257
|
+
const object = c === "{";
|
|
258
|
+
const entries = [];
|
|
259
|
+
const items = [];
|
|
260
|
+
i++;
|
|
261
|
+
blanks();
|
|
262
|
+
if (text[i] === (object ? "}" : "]")) {
|
|
263
|
+
i++;
|
|
264
|
+
return object ? { kind: "object", entries } : { kind: "array", items };
|
|
265
|
+
}
|
|
266
|
+
for (;;) {
|
|
267
|
+
let key = "";
|
|
268
|
+
if (object) {
|
|
269
|
+
blanks();
|
|
270
|
+
if (text[i] !== '"')
|
|
271
|
+
return undefined;
|
|
272
|
+
const k = string();
|
|
273
|
+
if (!k || k.kind !== "string")
|
|
274
|
+
return undefined;
|
|
275
|
+
key = k.value;
|
|
276
|
+
blanks();
|
|
277
|
+
if (text[i++] !== ":")
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
const v = value(depth + 1);
|
|
281
|
+
if (!v)
|
|
282
|
+
return undefined;
|
|
283
|
+
if (object)
|
|
284
|
+
entries.push([key, v]);
|
|
285
|
+
else
|
|
286
|
+
items.push(v);
|
|
287
|
+
blanks();
|
|
288
|
+
if (text[i] === ",") {
|
|
289
|
+
i++;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (text[i] !== (object ? "}" : "]"))
|
|
293
|
+
return undefined;
|
|
294
|
+
i++;
|
|
295
|
+
return object ? { kind: "object", entries } : { kind: "array", items };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
LITERAL.lastIndex = i;
|
|
299
|
+
const literal = LITERAL.exec(text);
|
|
300
|
+
if (!literal)
|
|
301
|
+
return undefined;
|
|
302
|
+
i += literal[0].length;
|
|
303
|
+
return {
|
|
304
|
+
kind: "literal",
|
|
305
|
+
value: literal[0] in KEYWORDS ? KEYWORDS[literal[0]] : Number(literal[0]),
|
|
306
|
+
start: i - literal[0].length,
|
|
307
|
+
end: i,
|
|
308
|
+
};
|
|
309
|
+
};
|
|
310
|
+
const node = value(0);
|
|
311
|
+
if (!node && progress)
|
|
312
|
+
progress.reached = i;
|
|
313
|
+
return node ? { node, end: i } : undefined;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* The JSON documents in a text (API responses, `kubectl … -o json`, `terraform output -json`): values that start a
|
|
317
|
+
* line. A parsed document's nested objects aren't tried again. (What the starts that aren't JSON read is bounded, four
|
|
318
|
+
* times the text, so the scan stays linear: a log's `[INFO] …` lines fail at once, and any number of them is tried.
|
|
319
|
+
* Past the bound, a start inside what a failed one read is skipped, and the ones after it are tried: `[\n` repeated
|
|
320
|
+
* hides no document after it.)
|
|
321
|
+
*/
|
|
322
|
+
export function* jsonDocuments(text) {
|
|
323
|
+
let budget = 4 * text.length + 4096;
|
|
324
|
+
let resume = 0;
|
|
325
|
+
const progress = { reached: 0 };
|
|
326
|
+
for (const m of text.matchAll(/(?:^|\n)[ \t]*([{[])/dg)) {
|
|
327
|
+
const [start] = m.indices[1];
|
|
328
|
+
if (start < resume)
|
|
329
|
+
continue;
|
|
330
|
+
const doc = parseJson(text, start, 100, progress);
|
|
331
|
+
if (!doc) {
|
|
332
|
+
budget -= progress.reached - start + 1;
|
|
333
|
+
if (budget < 0)
|
|
334
|
+
resume = Math.max(resume, progress.reached);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
resume = doc.end;
|
|
338
|
+
yield doc.node;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/** The `{…}` object around [start, end), braces balanced (braces inside its strings too), within `max` characters on each side; "" if none. */
|
|
342
|
+
function enclosingObject(text, start, end, max = 2000) {
|
|
343
|
+
let depth = 0;
|
|
344
|
+
let open = -1;
|
|
345
|
+
for (let i = start - 1; i >= Math.max(0, start - max); i--) {
|
|
346
|
+
if (text[i] === "}")
|
|
347
|
+
depth++;
|
|
348
|
+
else if (text[i] === "{") {
|
|
349
|
+
if (depth === 0) {
|
|
350
|
+
open = i;
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
depth--;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (open < 0)
|
|
357
|
+
return "";
|
|
358
|
+
depth = 0;
|
|
359
|
+
for (let i = end; i < Math.min(text.length, end + max); i++) {
|
|
360
|
+
if (text[i] === "{")
|
|
361
|
+
depth++;
|
|
362
|
+
else if (text[i] === "}") {
|
|
363
|
+
if (depth === 0)
|
|
364
|
+
return text.slice(open, i + 1);
|
|
365
|
+
depth--;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return "";
|
|
369
|
+
}
|
|
370
|
+
/** A string literal, an identifier, or a bracket: what an object literal's keys and nesting are read from. */
|
|
371
|
+
const OBJECT_TOKEN = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[A-Za-z_$][\w$]*|[{[(]|[}\])]/g;
|
|
372
|
+
/** Whether `object` (a `{…}` literal, JSON or a JS or Python one) has a member named `key` of its own: not in a child, not in a string. */
|
|
373
|
+
function hasOwnKey(object, key) {
|
|
374
|
+
const colon = /\s*:/y;
|
|
375
|
+
let depth = 0;
|
|
376
|
+
for (const m of object.matchAll(OBJECT_TOKEN)) {
|
|
377
|
+
if (m[0].length === 1 && "{[(".includes(m[0]))
|
|
378
|
+
depth++;
|
|
379
|
+
else if (m[0].length === 1 && "}])".includes(m[0]))
|
|
380
|
+
depth--;
|
|
381
|
+
else if (depth === 1 && m[0].replace(/^["']|["']$/g, "") === key) {
|
|
382
|
+
colon.lastIndex = m.index + m[0].length;
|
|
383
|
+
if (colon.test(object))
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
/** Whether [start, end) is a member of a JSON Web Key: its innermost object has a `kty` of its own (JSON, or a JS or Python literal). */
|
|
390
|
+
export function inJwk(text, start, end) {
|
|
391
|
+
return hasOwnKey(enclosingObject(text, start, end), "kty");
|
|
392
|
+
}
|
package/dist/known.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Finding } from "./types.ts";
|
|
2
|
+
export declare class KnownValues {
|
|
3
|
+
private long;
|
|
4
|
+
private ends;
|
|
5
|
+
private short;
|
|
6
|
+
/** Short base64 pieces of values, found anywhere: inside a blob they have base64 on both sides. */
|
|
7
|
+
private shortB64;
|
|
8
|
+
/** Every short form in the order added; `word` if it's matched as a whole word (`short`), else anywhere (`shortB64`). */
|
|
9
|
+
private shortForms;
|
|
10
|
+
/** The short forms indexed: all up to `mainCount` in one index, the rest in a small one (see rebuild). */
|
|
11
|
+
private main?;
|
|
12
|
+
private mainCount;
|
|
13
|
+
private recent?;
|
|
14
|
+
private labels;
|
|
15
|
+
/** The value each form was made from: a form found is the value written another way, and tagged as the value. */
|
|
16
|
+
private origins;
|
|
17
|
+
/** The values added, as given (trimmed). */
|
|
18
|
+
private values;
|
|
19
|
+
/** Changes whenever a value not held before is added: what was worked out under an earlier set is told from what holds now. */
|
|
20
|
+
version: number;
|
|
21
|
+
/** How many values it holds. */
|
|
22
|
+
get size(): number;
|
|
23
|
+
/** Adds a secret value. Values too short or too ordinary to replace everywhere are ignored. (One already held keeps its first label.) */
|
|
24
|
+
add(value: string, label?: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Indexes the short forms added since the last find. They go in a small index of their own, so learning a value and
|
|
27
|
+
* then finding (a session that learns as it goes) costs that small index, not all of them; once it holds more than
|
|
28
|
+
* RECENT_FORMS, every form is indexed in one again.
|
|
29
|
+
*/
|
|
30
|
+
private rebuild;
|
|
31
|
+
find(text: string): Finding[];
|
|
32
|
+
}
|
|
33
|
+
/** Plain words and very short values would match ordinary text everywhere. */
|
|
34
|
+
export declare function isReplaceable(value: string): boolean;
|