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/known.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Values that are secret wherever they appear, because the caller says so: the credentials in its
|
|
3
|
+
* environment and config, or values found earlier in the same transcript. Each is also matched in the
|
|
4
|
+
* forms output turns it into: JSON- and URL-escaped, shell- and batch-quoted, hex, and base64 (also inside a
|
|
5
|
+
* longer base64 blob, as in a Basic auth header). A token value of 24 or more characters cut short
|
|
6
|
+
* (`head -c`, a truncated log) is caught by its first or last 16 characters.
|
|
7
|
+
*
|
|
8
|
+
* Values of 16+ characters are found by a rolling hash over 16-character windows, shorter ones by
|
|
9
|
+
* Aho-Corasick: both linear in the text whatever the number of values.
|
|
10
|
+
*/
|
|
11
|
+
import { naturalness } from "./charlm.js";
|
|
12
|
+
import { KeywordSet } from "./aho.js";
|
|
13
|
+
import { continuesToken } from "./syntax.js";
|
|
14
|
+
const W = 16;
|
|
15
|
+
function base64(bytes, url = false) {
|
|
16
|
+
let bin = "";
|
|
17
|
+
for (const b of bytes)
|
|
18
|
+
bin += String.fromCharCode(b);
|
|
19
|
+
const s = btoa(bin);
|
|
20
|
+
return url ? s.replace(/\+/g, "-").replace(/\//g, "_") : s;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The value's base64 as it appears inside a longer blob: the value can start at any byte offset mod 3,
|
|
24
|
+
* so each of the three alignments is encoded, and the characters at the edges, which depend on the
|
|
25
|
+
* neighbouring bytes, are dropped.
|
|
26
|
+
*/
|
|
27
|
+
function base64Shifts(value) {
|
|
28
|
+
const bytes = new TextEncoder().encode(value);
|
|
29
|
+
const out = [];
|
|
30
|
+
for (let shift = 0; shift < 3; shift++) {
|
|
31
|
+
const padded = new Uint8Array(shift + bytes.length);
|
|
32
|
+
padded.set(bytes, shift);
|
|
33
|
+
for (const url of [false, true]) {
|
|
34
|
+
let s = base64(padded, url).replace(/=+$/, "");
|
|
35
|
+
// Leading characters mixing pad bytes and value bits.
|
|
36
|
+
s = s.slice(shift === 0 ? 0 : shift === 1 ? 2 : 3);
|
|
37
|
+
// Trailing characters that depend on what follows the value.
|
|
38
|
+
const tail = (shift + bytes.length) % 3;
|
|
39
|
+
if (tail)
|
|
40
|
+
s = s.slice(0, -1);
|
|
41
|
+
if (s.length >= 8)
|
|
42
|
+
out.push(s);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/** The forms a known value takes in output, besides itself. */
|
|
48
|
+
function variants(value) {
|
|
49
|
+
const out = new Set([value]);
|
|
50
|
+
const json = JSON.stringify(value).slice(1, -1);
|
|
51
|
+
out.add(json);
|
|
52
|
+
out.add(json.replace(/\//g, "\\/"));
|
|
53
|
+
out.add(json.replace(/[&<>']/g, (c) => `\\u00${c.charCodeAt(0).toString(16).padStart(2, "0")}`));
|
|
54
|
+
out.add(JSON.stringify(json).slice(1, -1)); // twice-escaped: JSON inside a JSON string
|
|
55
|
+
let uri = value;
|
|
56
|
+
try {
|
|
57
|
+
uri = encodeURIComponent(value);
|
|
58
|
+
}
|
|
59
|
+
catch { } // lone surrogates
|
|
60
|
+
out.add(uri);
|
|
61
|
+
out.add(uri.replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`).replace(/%20/g, "+"));
|
|
62
|
+
out.add(uri.toLowerCase() === uri ? uri : uri.replace(/%[0-9A-F]{2}/g, (m) => m.toLowerCase()));
|
|
63
|
+
out.add(value.replace(/([^\w\-.,:/@+=%])/g, "\\$1")); // printf %q
|
|
64
|
+
out.add(value.replace(/(["\\$`])/g, "\\$1")); // inside double quotes
|
|
65
|
+
out.add(value.replace(/'/g, "'\\''")); // inside single quotes
|
|
66
|
+
out.add(value.replace(/'/g, `'"'"'`)); // inside single quotes, the other way
|
|
67
|
+
out.add(value.replace(/[\^&|<>()%!]/g, "^$&")); // Windows batch
|
|
68
|
+
out.add(value.replace(/'/g, "''")); // SQL, YAML single quotes
|
|
69
|
+
out.add(value.replace(/\$/g, "$$$$")); // docker compose
|
|
70
|
+
out.add(value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """)); // HTML, XML
|
|
71
|
+
const bytes = new TextEncoder().encode(value);
|
|
72
|
+
let hex = "";
|
|
73
|
+
for (const b of bytes)
|
|
74
|
+
hex += b.toString(16).padStart(2, "0");
|
|
75
|
+
out.add(hex);
|
|
76
|
+
out.add(hex.toUpperCase());
|
|
77
|
+
if (/^[0-9a-f]+$/i.test(value)) {
|
|
78
|
+
out.add(value.toLowerCase());
|
|
79
|
+
out.add(value.toUpperCase());
|
|
80
|
+
}
|
|
81
|
+
// Standalone `base64(value)`, padded, in both alphabets.
|
|
82
|
+
out.add(base64(bytes));
|
|
83
|
+
out.add(base64(bytes, true));
|
|
84
|
+
// A multi-line value (a key, a certificate bundle) also leaks line by line.
|
|
85
|
+
// (Only lines that read as random: a PEM body's, not a JSON or .env document's boilerplate.)
|
|
86
|
+
if (value.includes("\n")) {
|
|
87
|
+
for (const line of value.split(/\r?\n/)) {
|
|
88
|
+
const t = line.trim();
|
|
89
|
+
if (t.length >= W && !/\s/.test(t) && (/^[A-Za-z0-9+/=_-]+$/.test(t) || naturalness(t) < 0.5))
|
|
90
|
+
out.add(t);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return [...out].filter((v) => v.length >= 6);
|
|
94
|
+
}
|
|
95
|
+
function hash(s, start) {
|
|
96
|
+
let h = 0;
|
|
97
|
+
for (let i = start; i < start + W; i++)
|
|
98
|
+
h = (Math.imul(h, 31) + s.charCodeAt(i)) | 0;
|
|
99
|
+
return h;
|
|
100
|
+
}
|
|
101
|
+
/** The polynomial hash, base 31, of `s[start, end)`: the whole-value hash of LiteralIndex. */
|
|
102
|
+
function fullHash(s, start = 0, end = s.length) {
|
|
103
|
+
let h = 0;
|
|
104
|
+
for (let i = start; i < end; i++)
|
|
105
|
+
h = (Math.imul(h, 31) + s.charCodeAt(i)) | 0;
|
|
106
|
+
return h;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Finds every occurrence of any of many literal strings (16+ characters), in linear time: a rolling hash of a
|
|
110
|
+
* position's first 16 characters picks the values starting with them, kept by length; each length is checked by the
|
|
111
|
+
* candidate's whole hash (from the text's prefix hashes, built on the first hit) before the string itself, so values
|
|
112
|
+
* that share their start (JWTs' header, a vendor's prefix and more) cost a constant per length, not a copy.
|
|
113
|
+
*/
|
|
114
|
+
class LiteralIndex {
|
|
115
|
+
buckets = new Map();
|
|
116
|
+
get size() {
|
|
117
|
+
return this.buckets.size;
|
|
118
|
+
}
|
|
119
|
+
add(value) {
|
|
120
|
+
const h = hash(value, 0);
|
|
121
|
+
let bucket = this.buckets.get(h);
|
|
122
|
+
if (!bucket)
|
|
123
|
+
this.buckets.set(h, (bucket = { lengths: [], values: new Map() }));
|
|
124
|
+
let same = bucket.values.get(value.length);
|
|
125
|
+
if (!same) {
|
|
126
|
+
bucket.values.set(value.length, (same = new Map()));
|
|
127
|
+
bucket.lengths.push(value.length);
|
|
128
|
+
bucket.lengths.sort((a, b) => b - a);
|
|
129
|
+
}
|
|
130
|
+
const full = fullHash(value);
|
|
131
|
+
const list = same.get(full);
|
|
132
|
+
if (!list)
|
|
133
|
+
same.set(full, [value]);
|
|
134
|
+
else if (!list.includes(value))
|
|
135
|
+
list.push(value);
|
|
136
|
+
}
|
|
137
|
+
find(text) {
|
|
138
|
+
const found = [];
|
|
139
|
+
if (text.length < W || !this.buckets.size)
|
|
140
|
+
return found;
|
|
141
|
+
let pow = 1;
|
|
142
|
+
for (let i = 0; i < W - 1; i++)
|
|
143
|
+
pow = Math.imul(pow, 31);
|
|
144
|
+
// (The text's prefix hashes and powers of 31, for any substring's hash in O(1).)
|
|
145
|
+
let prefix;
|
|
146
|
+
let powers;
|
|
147
|
+
const substringHash = (from, to) => {
|
|
148
|
+
if (!prefix || !powers) {
|
|
149
|
+
prefix = new Int32Array(text.length + 1);
|
|
150
|
+
powers = new Int32Array(text.length + 1);
|
|
151
|
+
powers[0] = 1;
|
|
152
|
+
for (let k = 0; k < text.length; k++) {
|
|
153
|
+
prefix[k + 1] = (Math.imul(prefix[k], 31) + text.charCodeAt(k)) | 0;
|
|
154
|
+
powers[k + 1] = Math.imul(powers[k], 31);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return (prefix[to] - Math.imul(prefix[from], powers[to - from])) | 0;
|
|
158
|
+
};
|
|
159
|
+
let h = hash(text, 0);
|
|
160
|
+
for (let i = 0; i + W <= text.length;) {
|
|
161
|
+
const bucket = this.buckets.get(h);
|
|
162
|
+
if (bucket) {
|
|
163
|
+
const length = bucket.lengths.find((n) => i + n <= text.length &&
|
|
164
|
+
bucket.values
|
|
165
|
+
.get(n)
|
|
166
|
+
.get(substringHash(i, i + n))
|
|
167
|
+
?.includes(text.slice(i, i + n)));
|
|
168
|
+
// Keep rolling after a hit: another value may start inside this one and run past it.
|
|
169
|
+
if (length !== undefined)
|
|
170
|
+
found.push([i, text.slice(i, i + length)]);
|
|
171
|
+
}
|
|
172
|
+
if (i + W >= text.length)
|
|
173
|
+
break;
|
|
174
|
+
h = (Math.imul(h - Math.imul(text.charCodeAt(i), pow), 31) + text.charCodeAt(i + W)) | 0;
|
|
175
|
+
i++;
|
|
176
|
+
}
|
|
177
|
+
return found;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const TOKEN_CHAR = /[A-Za-z0-9+/_-]/;
|
|
181
|
+
/** How many short forms are indexed apart from the rest before all are indexed in one again. */
|
|
182
|
+
const RECENT_FORMS = 256;
|
|
183
|
+
export class KnownValues {
|
|
184
|
+
long = new LiteralIndex();
|
|
185
|
+
ends = new LiteralIndex();
|
|
186
|
+
short = new Map();
|
|
187
|
+
/** Short base64 pieces of values, found anywhere: inside a blob they have base64 on both sides. */
|
|
188
|
+
shortB64 = new Map();
|
|
189
|
+
/** Every short form in the order added; `word` if it's matched as a whole word (`short`), else anywhere (`shortB64`). */
|
|
190
|
+
shortForms = [];
|
|
191
|
+
/** The short forms indexed: all up to `mainCount` in one index, the rest in a small one (see rebuild). */
|
|
192
|
+
main;
|
|
193
|
+
mainCount = 0;
|
|
194
|
+
recent;
|
|
195
|
+
labels = new Map();
|
|
196
|
+
/** The value each form was made from: a form found is the value written another way, and tagged as the value. */
|
|
197
|
+
origins = new Map();
|
|
198
|
+
/** The values added, as given (trimmed). */
|
|
199
|
+
values = new Set();
|
|
200
|
+
/** Changes whenever a value not held before is added: what was worked out under an earlier set is told from what holds now. */
|
|
201
|
+
version = 0;
|
|
202
|
+
/** How many values it holds. */
|
|
203
|
+
get size() {
|
|
204
|
+
return this.values.size;
|
|
205
|
+
}
|
|
206
|
+
/** Adds a secret value. Values too short or too ordinary to replace everywhere are ignored. (One already held keeps its first label.) */
|
|
207
|
+
add(value, label = "known secret") {
|
|
208
|
+
value = value.trim();
|
|
209
|
+
if (!isReplaceable(value))
|
|
210
|
+
return false;
|
|
211
|
+
if (this.values.has(value))
|
|
212
|
+
return true;
|
|
213
|
+
this.values.add(value);
|
|
214
|
+
this.version++;
|
|
215
|
+
// The first label given for a form is kept, for full and short forms alike.
|
|
216
|
+
const form = (v) => {
|
|
217
|
+
if (!this.labels.has(v))
|
|
218
|
+
this.labels.set(v, label);
|
|
219
|
+
if (!this.origins.has(v))
|
|
220
|
+
this.origins.set(v, value);
|
|
221
|
+
};
|
|
222
|
+
for (const v of variants(value)) {
|
|
223
|
+
form(v);
|
|
224
|
+
if (v.length >= W)
|
|
225
|
+
this.long.add(v);
|
|
226
|
+
else if (!this.short.has(v)) {
|
|
227
|
+
this.short.set(v, label);
|
|
228
|
+
this.shortForms.push({ form: v, word: true });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
for (const v of base64Shifts(value)) {
|
|
232
|
+
form(v);
|
|
233
|
+
if (v.length >= W)
|
|
234
|
+
this.long.add(v);
|
|
235
|
+
// (Also when the same string is a whole-word short form: inside a longer blob it isn't a word.)
|
|
236
|
+
else if (!this.shortB64.has(v)) {
|
|
237
|
+
this.shortB64.set(v, label);
|
|
238
|
+
this.shortForms.push({ form: v, word: false });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Only values long enough that a 16-character end is still unmistakably theirs.
|
|
242
|
+
if (value.length >= 24 && /^[A-Za-z0-9+/=_-]+$/.test(value)) {
|
|
243
|
+
for (const end of [value.slice(0, W), value.slice(-W)]) {
|
|
244
|
+
this.ends.add(end);
|
|
245
|
+
form(end);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Indexes the short forms added since the last find. They go in a small index of their own, so learning a value and
|
|
252
|
+
* then finding (a session that learns as it goes) costs that small index, not all of them; once it holds more than
|
|
253
|
+
* RECENT_FORMS, every form is indexed in one again.
|
|
254
|
+
*/
|
|
255
|
+
rebuild() {
|
|
256
|
+
const indexed = this.mainCount + (this.recent?.forms.length ?? 0);
|
|
257
|
+
if (indexed === this.shortForms.length)
|
|
258
|
+
return;
|
|
259
|
+
const index = (forms) => ({
|
|
260
|
+
set: new KeywordSet(forms.map((f) => f.form), true),
|
|
261
|
+
forms,
|
|
262
|
+
});
|
|
263
|
+
if (this.shortForms.length - this.mainCount > RECENT_FORMS) {
|
|
264
|
+
this.main = index(this.shortForms.slice());
|
|
265
|
+
this.mainCount = this.shortForms.length;
|
|
266
|
+
this.recent = undefined;
|
|
267
|
+
}
|
|
268
|
+
else
|
|
269
|
+
this.recent = index(this.shortForms.slice(this.mainCount));
|
|
270
|
+
}
|
|
271
|
+
find(text) {
|
|
272
|
+
this.rebuild();
|
|
273
|
+
const out = [];
|
|
274
|
+
for (const [start, v] of this.long.find(text))
|
|
275
|
+
out.push({
|
|
276
|
+
start,
|
|
277
|
+
end: start + v.length,
|
|
278
|
+
kind: this.labels.get(v) ?? "known secret",
|
|
279
|
+
rule: "known",
|
|
280
|
+
confidence: 1,
|
|
281
|
+
value: this.origins.get(v),
|
|
282
|
+
});
|
|
283
|
+
// Short values whole only (or straight after a flag like `-p`); short base64 pieces anywhere.
|
|
284
|
+
for (const index of [this.main, this.recent]) {
|
|
285
|
+
if (!index)
|
|
286
|
+
continue;
|
|
287
|
+
for (const [at, k] of index.set.matches(text)) {
|
|
288
|
+
const { form: v, word } = index.forms[k];
|
|
289
|
+
const dash = v.includes("-");
|
|
290
|
+
if (word &&
|
|
291
|
+
(continuesToken(text, at + v.length, dash) ||
|
|
292
|
+
(continuesToken(text, at - 1, dash) && !/(?:^|[\s'"])-[A-Za-z]$/.test(text.slice(Math.max(0, at - 3), at)))))
|
|
293
|
+
continue;
|
|
294
|
+
out.push({
|
|
295
|
+
start: at,
|
|
296
|
+
end: at + v.length,
|
|
297
|
+
kind: (word ? this.short.get(v) : this.shortB64.get(v)) ?? "known secret",
|
|
298
|
+
rule: "known",
|
|
299
|
+
confidence: 1,
|
|
300
|
+
value: this.origins.get(v),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// What's left of a value that the output cut short: the whole run of token characters around an end.
|
|
305
|
+
// Hits come in text order; a hit inside a full match, or inside the run just expanded, adds nothing.
|
|
306
|
+
const full = out.slice().sort((a, b) => a.start - b.start);
|
|
307
|
+
let fi = 0;
|
|
308
|
+
let fullEnd = -1;
|
|
309
|
+
let lastEnd = -1;
|
|
310
|
+
for (const [at, v] of this.ends.find(text)) {
|
|
311
|
+
while (fi < full.length && full[fi].start <= at)
|
|
312
|
+
fullEnd = Math.max(fullEnd, full[fi++].end);
|
|
313
|
+
if (fullEnd >= at + W || at < lastEnd)
|
|
314
|
+
continue;
|
|
315
|
+
let start = at;
|
|
316
|
+
let end = at + W;
|
|
317
|
+
while (start > 0 && TOKEN_CHAR.test(text[start - 1]))
|
|
318
|
+
start--;
|
|
319
|
+
while (end < text.length && TOKEN_CHAR.test(text[end]))
|
|
320
|
+
end++;
|
|
321
|
+
lastEnd = end;
|
|
322
|
+
out.push({
|
|
323
|
+
start,
|
|
324
|
+
end,
|
|
325
|
+
kind: `part of ${this.labels.get(v) ?? "known secret"}`,
|
|
326
|
+
rule: "known:partial",
|
|
327
|
+
confidence: 0.95,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/** Plain words and very short values would match ordinary text everywhere. */
|
|
334
|
+
export function isReplaceable(value) {
|
|
335
|
+
if (value.length < 6 || value.length > 16_384)
|
|
336
|
+
return false;
|
|
337
|
+
if (/^\p{L}+$/u.test(value))
|
|
338
|
+
return value.length >= 14;
|
|
339
|
+
return !/^\d{1,8}$/.test(value);
|
|
340
|
+
}
|
package/dist/lines.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A text's lines, where a position's line starts and ends, the run of text a position ends (read leftward), and where
|
|
3
|
+
* a position falls among sorted offsets. Each bound scans no further than a fixed distance (2,000 characters unless
|
|
4
|
+
* the caller asks for more), so lookups per match stay linear on text with very long lines or none at all.
|
|
5
|
+
*/
|
|
6
|
+
/** A text's lines (split at `\n`) and where each starts. */
|
|
7
|
+
export declare function splitLines(text: string): {
|
|
8
|
+
lines: string[];
|
|
9
|
+
starts: number[];
|
|
10
|
+
};
|
|
11
|
+
/** Where the line `at` is on starts, looking back no further than `max` characters. */
|
|
12
|
+
export declare function lineStart(text: string, at: number, max?: number): number;
|
|
13
|
+
/** Where the line `at` is on ends (its line break), looking ahead no further than `max` characters. */
|
|
14
|
+
export declare function lineEnd(text: string, at: number, max?: number): number;
|
|
15
|
+
/**
|
|
16
|
+
* The end of `text` after the last character that `stop` matches (the token a value continues, read leftward): a
|
|
17
|
+
* backward scan, where `/[^…]*$/` tries again from every position.
|
|
18
|
+
*/
|
|
19
|
+
export declare function trailingRun(text: string, stop: RegExp): string;
|
|
20
|
+
/**
|
|
21
|
+
* `build` for the last text it was asked about, kept until it is asked about another: the per-text indexes readers
|
|
22
|
+
* build (line starts, bracket closes, a line's commands, value stops, YAML blocks), shared by every rule that reads one
|
|
23
|
+
* text and rebuilt when detection moves on to another (a decoded literal, the next text). The last text and its index
|
|
24
|
+
* stay alive until then.
|
|
25
|
+
*/
|
|
26
|
+
export declare function lastText<T>(build: (text: string) => T): (text: string) => T;
|
|
27
|
+
/** The first index of a sorted array whose value is at least `value`. */
|
|
28
|
+
export declare function lowerBound(sorted: ArrayLike<number>, value: number): number;
|
package/dist/lines.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A text's lines, where a position's line starts and ends, the run of text a position ends (read leftward), and where
|
|
3
|
+
* a position falls among sorted offsets. Each bound scans no further than a fixed distance (2,000 characters unless
|
|
4
|
+
* the caller asks for more), so lookups per match stay linear on text with very long lines or none at all.
|
|
5
|
+
*/
|
|
6
|
+
/** A text's lines (split at `\n`) and where each starts. */
|
|
7
|
+
export function splitLines(text) {
|
|
8
|
+
const lines = text.split("\n");
|
|
9
|
+
const starts = [];
|
|
10
|
+
let at = 0;
|
|
11
|
+
for (const l of lines) {
|
|
12
|
+
starts.push(at);
|
|
13
|
+
at += l.length + 1;
|
|
14
|
+
}
|
|
15
|
+
return { lines, starts };
|
|
16
|
+
}
|
|
17
|
+
/** Where the line `at` is on starts, looking back no further than `max` characters. */
|
|
18
|
+
export function lineStart(text, at, max = 2000) {
|
|
19
|
+
const from = Math.max(0, at - max);
|
|
20
|
+
for (let i = at - 1; i >= from; i--)
|
|
21
|
+
if (text.charCodeAt(i) === 10)
|
|
22
|
+
return i + 1;
|
|
23
|
+
return from;
|
|
24
|
+
}
|
|
25
|
+
/** Where the line `at` is on ends (its line break), looking ahead no further than `max` characters. */
|
|
26
|
+
export function lineEnd(text, at, max = 2000) {
|
|
27
|
+
const limit = Math.min(text.length, at + max);
|
|
28
|
+
for (let i = at; i < limit; i++)
|
|
29
|
+
if (text.charCodeAt(i) === 10)
|
|
30
|
+
return i;
|
|
31
|
+
return limit;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The end of `text` after the last character that `stop` matches (the token a value continues, read leftward): a
|
|
35
|
+
* backward scan, where `/[^…]*$/` tries again from every position.
|
|
36
|
+
*/
|
|
37
|
+
export function trailingRun(text, stop) {
|
|
38
|
+
let i = text.length;
|
|
39
|
+
while (i > 0 && !stop.test(text[i - 1]))
|
|
40
|
+
i--;
|
|
41
|
+
return text.slice(i);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* `build` for the last text it was asked about, kept until it is asked about another: the per-text indexes readers
|
|
45
|
+
* build (line starts, bracket closes, a line's commands, value stops, YAML blocks), shared by every rule that reads one
|
|
46
|
+
* text and rebuilt when detection moves on to another (a decoded literal, the next text). The last text and its index
|
|
47
|
+
* stay alive until then.
|
|
48
|
+
*/
|
|
49
|
+
export function lastText(build) {
|
|
50
|
+
let last;
|
|
51
|
+
return (text) => {
|
|
52
|
+
if (last?.text !== text)
|
|
53
|
+
last = { text, value: build(text) };
|
|
54
|
+
return last.value;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** The first index of a sorted array whose value is at least `value`. */
|
|
58
|
+
export function lowerBound(sorted, value) {
|
|
59
|
+
let lo = 0;
|
|
60
|
+
let hi = sorted.length;
|
|
61
|
+
while (lo < hi) {
|
|
62
|
+
const mid = (lo + hi) >> 1;
|
|
63
|
+
if (sorted[mid] < value)
|
|
64
|
+
lo = mid + 1;
|
|
65
|
+
else
|
|
66
|
+
hi = mid;
|
|
67
|
+
}
|
|
68
|
+
return lo;
|
|
69
|
+
}
|
package/dist/lookup.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A lookup table for keys that come from the text: no prototype, so a key like `constructor` or `toString` finds
|
|
3
|
+
* nothing (`ENTITIES["constructor"]` on a plain object is `Object` itself), and frozen.
|
|
4
|
+
*/
|
|
5
|
+
export declare function lookup<T>(entries: Record<string, T>): Readonly<Record<string, T | undefined>>;
|
package/dist/lookup.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A lookup table for keys that come from the text: no prototype, so a key like `constructor` or `toString` finds
|
|
3
|
+
* nothing (`ENTITIES["constructor"]` on a plain object is `Object` itself), and frozen.
|
|
4
|
+
*/
|
|
5
|
+
export function lookup(entries) {
|
|
6
|
+
return Object.freeze(Object.assign(Object.create(null), entries));
|
|
7
|
+
}
|