@blamejs/core 0.18.35 → 0.18.37

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.
@@ -41,6 +41,7 @@ var C = require("../constants");
41
41
  var pick = require("../pick");
42
42
  var numericBounds = require("../numeric-bounds");
43
43
  var safeBuffer = require("../safe-buffer");
44
+ var codepointClass = require("../codepoint-class");
44
45
  var { defineClass } = require("../framework-error");
45
46
 
46
47
  var IniSafeError = defineClass("IniSafeError", { alwaysPermanent: true });
@@ -69,7 +70,7 @@ function _stripComment(line) {
69
70
  if (c === "\"" && !inSingle) { inDouble = !inDouble; continue; }
70
71
  if (c === "'" && !inDouble) { inSingle = !inSingle; continue; }
71
72
  if (!inSingle && !inDouble && (c === ";" || c === "#")) {
72
- if (i === 0 || /\s/.test(line.charAt(i - 1))) {
73
+ if (i === 0 || codepointClass.inRanges(line.charCodeAt(i - 1), codepointClass.WHITESPACE_RANGES)) {
73
74
  return line.slice(0, i);
74
75
  }
75
76
  }
@@ -110,6 +111,113 @@ function _unquote(raw) {
110
111
  return s;
111
112
  }
112
113
 
114
+ // Lexical shape tests for the value forms INI coerces. Each replaces an
115
+ // anchored pattern and walks the string once, so the cost is its length —
116
+ // values arrive from a file an operator may not control, and a screen whose
117
+ // cost depends on the arrangement of the characters is what this family avoids.
118
+ //
119
+ // `\d` in the patterns these replace is ASCII-only, so isAsciiDigit matches it
120
+ // exactly; a walk using a Unicode digit test would accept more than the pattern
121
+ // did and silently widen what parses as a number.
122
+
123
+ // `/^0x[0-9a-f]+$/i` — the prefix, then at least one hex digit, nothing else.
124
+ function _isHexInteger(s) {
125
+ if (s.length < 3) return false;
126
+ if (s.charAt(0) !== "0") return false;
127
+ var x = s.charAt(1);
128
+ if (x !== "x" && x !== "X") return false;
129
+ return codepointClass.isRunOf(s.slice(2), codepointClass.ASCII_HEX);
130
+ }
131
+
132
+ // Digits from `from` to the end, at least one. The shared tail of every shape
133
+ // below, and the reason none of them needs a quantifier.
134
+ function _digitsToEnd(s, from) {
135
+ if (from >= s.length) return false;
136
+ return codepointClass.isRunOf(s.slice(from), codepointClass.ASCII_DIGITS);
137
+ }
138
+
139
+ function _digitRunEnd(s, from) {
140
+ var i = from;
141
+ while (i < s.length && codepointClass.isAsciiDigit(s.charCodeAt(i))) i += 1;
142
+ return i; // === from when no digits
143
+ }
144
+
145
+ // `/^-?\d+$/`
146
+ function _isDecimalInteger(s) {
147
+ return _digitsToEnd(s, s.charAt(0) === "-" ? 1 : 0);
148
+ }
149
+
150
+ // `/^-?\d+\.\d+([eE][+-]?\d+)?$/` or `/^-?\d+[eE][+-]?\d+$/` — a float needs
151
+ // either a fractional part, an exponent, or both, which is what keeps a bare
152
+ // integer out of this branch and in the one above it.
153
+ function _isDecimalFloat(s) {
154
+ var i = s.charAt(0) === "-" ? 1 : 0;
155
+ var afterInt = _digitRunEnd(s, i);
156
+ if (afterInt === i) return false; // no integer part
157
+ i = afterInt;
158
+ var sawFraction = false;
159
+ if (s.charAt(i) === ".") {
160
+ var afterFrac = _digitRunEnd(s, i + 1);
161
+ if (afterFrac === i + 1) return false; // `1.` is not a float here
162
+ i = afterFrac;
163
+ sawFraction = true;
164
+ }
165
+ var e = s.charAt(i);
166
+ if (e === "e" || e === "E") {
167
+ var j = i + 1;
168
+ var sign = s.charAt(j);
169
+ if (sign === "+" || sign === "-") j += 1;
170
+ if (!_digitsToEnd(s, j)) return false;
171
+ return true; // exponent consumed the rest
172
+ }
173
+ return sawFraction && i === s.length;
174
+ }
175
+
176
+ // `[name "subsection"]` — the git-config form. Replaces
177
+ // `/^([A-Za-z0-9._-]+)\s+"([^"\\]*(?:\\.[^"\\]*)*)"$/`, whose inner group is the
178
+ // unrolled-loop idiom for a quoted string; walking it is both clearer and free
179
+ // of the backtracking that idiom exists to avoid.
180
+ //
181
+ // Two details of the pattern are load-bearing and preserved here. The value is
182
+ // captured VERBATIM, escapes included — `_unquote` does the unescaping later, so
183
+ // consuming the backslash here would double-unescape. And `\\.` cannot match a
184
+ // line terminator, so a backslash before one is not an escape and the whole
185
+ // header fails to match rather than swallowing the break.
186
+ var _SECTION_NAME_CHARS = codepointClass.ASCII_ALNUM + "._-";
187
+
188
+ function _isDotAtom(ch) {
189
+ // The characters `.` does NOT match without the `s` flag.
190
+ return ch !== "\n" && ch !== "\r" && ch !== "\u2028" && ch !== "\u2029";
191
+ }
192
+
193
+ function _parseQuotedSectionHeader(inner) {
194
+ var i = 0;
195
+ while (i < inner.length && _SECTION_NAME_CHARS.indexOf(inner.charAt(i)) !== -1) i += 1;
196
+ if (i === 0) return null; // name needs one char
197
+ var name = inner.slice(0, i);
198
+ var afterName = i;
199
+ while (i < inner.length &&
200
+ codepointClass.inRanges(inner.charCodeAt(i), codepointClass.WHITESPACE_RANGES)) i += 1;
201
+ if (i === afterName) return null; // `\s+` needs one
202
+ if (inner.charAt(i) !== "\"") return null;
203
+ i += 1;
204
+ var value = "";
205
+ while (i < inner.length) {
206
+ var ch = inner.charAt(i);
207
+ if (ch === "\\") {
208
+ var next = inner.charAt(i + 1);
209
+ if (i + 1 >= inner.length || !_isDotAtom(next)) return null;
210
+ value += ch + next;
211
+ i += 2;
212
+ continue;
213
+ }
214
+ if (ch === "\"") return i === inner.length - 1 ? [name, value] : null;
215
+ value += ch;
216
+ i += 1;
217
+ }
218
+ return null; // unterminated quote
219
+ }
220
+
113
221
  function _coerceValue(raw) {
114
222
  if (raw.length === 0) return raw;
115
223
  var first = raw.charAt(0);
@@ -117,21 +225,21 @@ function _coerceValue(raw) {
117
225
  var lower = raw.toLowerCase();
118
226
  if (TRUE_VALUES.has(lower)) return true;
119
227
  if (FALSE_VALUES.has(lower)) return false;
120
- if (/^0x[0-9a-f]+$/i.test(raw)) {
228
+ if (_isHexInteger(raw)) {
121
229
  var hex = parseInt(raw, RADIX_HEX);
122
230
  if (!Number.isSafeInteger(hex)) {
123
231
  throw _err("ini/value-out-of-range", "hex integer exceeds safe-integer range: " + raw);
124
232
  }
125
233
  return hex;
126
234
  }
127
- if (/^-?\d+$/.test(raw)) {
235
+ if (_isDecimalInteger(raw)) {
128
236
  var n = Number(raw);
129
237
  if (!Number.isSafeInteger(n)) {
130
238
  throw _err("ini/value-out-of-range", "integer exceeds safe-integer range: " + raw);
131
239
  }
132
240
  return n;
133
241
  }
134
- if (/^-?\d+\.\d+([eE][+-]?\d+)?$/.test(raw) || /^-?\d+[eE][+-]?\d+$/.test(raw)) {
242
+ if (_isDecimalFloat(raw)) {
135
243
  var f = Number(raw);
136
244
  // Float overflow (e.g. 1e999) coerces to ±Infinity — the same
137
245
  // never-silently-coerce refusal the integer/hex branches enforce with
@@ -178,10 +286,8 @@ function _parseSectionHeader(line) {
178
286
  if (inner.length === 0) {
179
287
  throw _err("ini/empty-section", "section header [] has no name");
180
288
  }
181
- var quotedMatch = /^([A-Za-z0-9._-]+)\s+"([^"\\]*(?:\\.[^"\\]*)*)"$/.exec(inner);
182
- if (quotedMatch) {
183
- return [quotedMatch[1], quotedMatch[2]];
184
- }
289
+ var quoted = _parseQuotedSectionHeader(inner);
290
+ if (quoted) return quoted;
185
291
  var parts = inner.split(".");
186
292
  for (var i = 0; i < parts.length; i++) {
187
293
  if (parts[i].length === 0) {
@@ -240,7 +346,7 @@ function parse(input, opts) {
240
346
  var sectionCount = 0;
241
347
  var keysInCurrentSection = 0;
242
348
 
243
- var lines = input.split(/\r?\n/);
349
+ var lines = codepointClass.splitLines(input);
244
350
  for (var li = 0; li < lines.length; li++) {
245
351
  var raw = lines[li];
246
352
  var stripped = _stripComment(raw).trim();
@@ -71,6 +71,61 @@ var RADIX_BIN = 0x2;
71
71
  var RADIX_OCTAL = 0x8;
72
72
  var RADIX_HEX = 0x10;
73
73
 
74
+ // Character sets and fixed-width shapes, as walks. TOML's grammar is all
75
+ // fixed-width date and time fields and single-character classes, so every one of
76
+ // these replaces a pattern with an index comparison — nothing here needs a
77
+ // search, and the parser's cost stays the length of the document.
78
+ var _OCTAL_DIGITS = "01234567";
79
+ var _BINARY_DIGITS = "01";
80
+ var _BARE_KEY_TAIL = codepointClass.ASCII_ALNUM + "_";
81
+ var _NUMBER_LEADS = codepointClass.ASCII_DIGITS + "+-i n"; // `inf`, `nan`, sign, space
82
+
83
+ function _isDigit(ch) { return ch.length === 1 && codepointClass.ASCII_DIGITS.indexOf(ch) !== -1; }
84
+ function _isHexDigit(ch) { return ch.length === 1 && codepointClass.ASCII_HEX.indexOf(ch) !== -1; }
85
+ function _isOctalDigit(ch) { return ch.length === 1 && _OCTAL_DIGITS.indexOf(ch) !== -1; }
86
+ function _isBinaryDigit(ch){ return ch.length === 1 && _BINARY_DIGITS.indexOf(ch) !== -1; }
87
+
88
+ // `charAt` past the end returns "", and `"".indexOf` is 0 on every string, so
89
+ // each of these guards its length before asking. Without it an out-of-range read
90
+ // answers "yes" and the shape checks accept a truncated document.
91
+ function _isBareKeyChar(ch) { return ch.length === 1 && _BARE_KEY_TAIL.indexOf(ch) !== -1; }
92
+ function _isNumberLead(ch) { return ch.length === 1 && _NUMBER_LEADS.indexOf(ch) !== -1; }
93
+
94
+ function _digitsAt(text, from, count) {
95
+ if (from + count > text.length) return false;
96
+ for (var i = 0; i < count; i += 1) {
97
+ if (!_isDigit(text.charAt(from + i))) return false;
98
+ }
99
+ return true;
100
+ }
101
+
102
+ // `\d{4}-\d{2}-\d{2}` anchored at the start of `text`.
103
+ function _looksLikeDate(text) {
104
+ return _digitsAt(text, 0, 4) && text.charAt(4) === "-" &&
105
+ _digitsAt(text, 5, 2) && text.charAt(7) === "-" &&
106
+ _digitsAt(text, 8, 2);
107
+ }
108
+
109
+ // `\d{2}:\d{2}:\d{2}` anchored at the start of `text`.
110
+ function _looksLikeTime(text) {
111
+ return _digitsAt(text, 0, 2) && text.charAt(2) === ":" &&
112
+ _digitsAt(text, 3, 2) && text.charAt(5) === ":" &&
113
+ _digitsAt(text, 6, 2);
114
+ }
115
+
116
+ // `^[+-]\d{2}:\d{2}$` — a whole numeric UTC offset, nothing after it.
117
+ function _isNumericOffset(text) {
118
+ if (text.length !== 6) return false;
119
+ var sign = text.charAt(0);
120
+ if (sign !== "+" && sign !== "-") return false;
121
+ return _digitsAt(text, 1, 2) && text.charAt(3) === ":" && _digitsAt(text, 4, 2);
122
+ }
123
+
124
+ // Strip the `_` digit separators TOML allows inside numbers.
125
+ function _stripUnderscores(text) {
126
+ return text.indexOf("_") === -1 ? text : codepointClass.stripRanges(text, [0x5F]);
127
+ }
128
+
74
129
  // Date-time literal character widths (per TOML / RFC 3339).
75
130
  var TIME_CHARS = 0x8; // "HH:MM:SS"
76
131
  var OFFSET_CHARS = 0x6; // "+HH:MM"
@@ -285,7 +340,7 @@ function parse(input, opts) {
285
340
  case "U": {
286
341
  var hexLen = 0x8;
287
342
  var hex8 = input.substring(pos, pos + hexLen);
288
- if (hex8.length < hexLen || !/^[0-9a-fA-F]{8}$/.test(hex8)) {
343
+ if (hex8.length < hexLen || hex8.length !== 8 || !codepointClass.isRunOf(hex8, codepointClass.ASCII_HEX)) {
289
344
  throw _err("bad \\U escape", "toml/bad-escape");
290
345
  }
291
346
  _advance(hexLen);
@@ -404,19 +459,19 @@ function parse(input, opts) {
404
459
  // current position doesn't start a date-time literal.
405
460
  function _tryParseDateTime() {
406
461
  // Date-time form: YYYY-MM-DD followed by 'T'/' ' followed by time
407
- if (pos + 10 <= len && /^\d{4}-\d{2}-\d{2}/.test(input.substr(pos, 10))) {
462
+ if (pos + 10 <= len && _looksLikeDate(input.substr(pos, 10))) {
408
463
  var sep = _peek(10);
409
464
  if (sep === "T" || sep === "t" || sep === " ") {
410
465
  // Look ahead for time portion (HH:MM:SS = 8 chars)
411
466
  var timeStart = pos + 11;
412
467
  var timeChars = TIME_CHARS;
413
- if (/^\d{2}:\d{2}:\d{2}/.test(input.substr(timeStart, timeChars))) {
468
+ if (_looksLikeTime(input.substr(timeStart, timeChars))) {
414
469
  var datePart = input.substr(pos, 10);
415
470
  var timeEnd = timeStart + timeChars;
416
471
  // Optional fractional
417
472
  if (_peek(timeEnd - pos) === ".") {
418
473
  timeEnd += 1;
419
- while (timeEnd < len && /\d/.test(input.charAt(timeEnd))) timeEnd += 1;
474
+ while (timeEnd < len && _isDigit(input.charAt(timeEnd))) timeEnd += 1;
420
475
  }
421
476
  var timePart = input.substring(timeStart, timeEnd);
422
477
  // Optional offset
@@ -427,7 +482,7 @@ function parse(input, opts) {
427
482
  // Expect ±HH:MM (6 chars)
428
483
  var offsetChars = OFFSET_CHARS;
429
484
  var off = input.substr(timeEnd, offsetChars);
430
- if (/^[+-]\d{2}:\d{2}$/.test(off)) {
485
+ if (_isNumericOffset(off)) {
431
486
  offsetStr = off;
432
487
  timeEnd += offsetChars;
433
488
  }
@@ -445,7 +500,7 @@ function parse(input, opts) {
445
500
  }
446
501
  // Date-only — but only if followed by something OTHER than digit/colon
447
502
  var after = _peek(10);
448
- if (!after || !/[0-9:.]/.test(after)) {
503
+ if (!after || !(_isDigit(after) || after === ":" || after === ".")) {
449
504
  var ds = input.substr(pos, 10);
450
505
  _advance(10);
451
506
  return { kind: "local-date", value: ds };
@@ -453,11 +508,11 @@ function parse(input, opts) {
453
508
  }
454
509
  // Time-only: HH:MM:SS (8 chars)
455
510
  var timeOnlyChars = TIME_CHARS;
456
- if (pos + timeOnlyChars <= len && /^\d{2}:\d{2}:\d{2}/.test(input.substr(pos, timeOnlyChars))) {
511
+ if (pos + timeOnlyChars <= len && _looksLikeTime(input.substr(pos, timeOnlyChars))) {
457
512
  var teEnd = pos + timeOnlyChars;
458
513
  if (input.charAt(teEnd) === ".") {
459
514
  teEnd += 1;
460
- while (teEnd < len && /\d/.test(input.charAt(teEnd))) teEnd += 1;
515
+ while (teEnd < len && _isDigit(input.charAt(teEnd))) teEnd += 1;
461
516
  }
462
517
  var ts = input.substring(pos, teEnd);
463
518
  _advance(teEnd - pos);
@@ -496,12 +551,12 @@ function parse(input, opts) {
496
551
  while (!_eof()) {
497
552
  var ch = _peek();
498
553
  if (ch === "_") { _advance(); continue; }
499
- if (radix === RADIX_HEX && /[0-9a-fA-F]/.test(ch)) { _advance(); continue; }
500
- if (radix === RADIX_OCTAL && /[0-7]/.test(ch)) { _advance(); continue; }
501
- if (radix === RADIX_BIN && /[01]/.test(ch)) { _advance(); continue; }
554
+ if (radix === RADIX_HEX && _isHexDigit(ch)) { _advance(); continue; }
555
+ if (radix === RADIX_OCTAL && _isOctalDigit(ch)) { _advance(); continue; }
556
+ if (radix === RADIX_BIN && _isBinaryDigit(ch)) { _advance(); continue; }
502
557
  break;
503
558
  }
504
- var digits = input.substring(digitsStart, pos).replace(/_/g, "");
559
+ var digits = _stripUnderscores(input.substring(digitsStart, pos));
505
560
  if (digits.length === 0) throw _err("expected digits after radix prefix", "toml/bad-number");
506
561
  var n = parseInt(digits, radix);
507
562
  if (!Number.isSafeInteger(n)) {
@@ -527,7 +582,7 @@ function parse(input, opts) {
527
582
  }
528
583
  break;
529
584
  }
530
- var raw = input.substring(startPos, pos).replace(/_/g, "");
585
+ var raw = _stripUnderscores(input.substring(startPos, pos));
531
586
  if (raw === "" || raw === "-" || raw === "+") {
532
587
  throw new SafeTomlError("invalid number", "toml/bad-number", startLine, startCol);
533
588
  }
@@ -560,17 +615,17 @@ function parse(input, opts) {
560
615
  if (c === "[") return _parseArray(depth + 1);
561
616
  if (c === "{") return _parseInlineTable(depth + 1);
562
617
 
563
- if (input.substr(pos, 4) === "true" && !/[A-Za-z0-9_]/.test(input.charAt(pos + 4) || "")) {
618
+ if (input.substr(pos, 4) === "true" && !_isBareKeyChar(input.charAt(pos + 4))) {
564
619
  _advance(4); return true;
565
620
  }
566
- if (input.substr(pos, 5) === "false" && !/[A-Za-z0-9_]/.test(input.charAt(pos + 5) || "")) {
621
+ if (input.substr(pos, 5) === "false" && !_isBareKeyChar(input.charAt(pos + 5))) {
567
622
  _advance(5); return false;
568
623
  }
569
624
 
570
625
  var dt = _tryParseDateTime();
571
626
  if (dt !== null) return dt.value;
572
627
 
573
- if (/[0-9+\-i n]/.test(c)) return _parseNumber(c);
628
+ if (_isNumberLead(c)) return _parseNumber(c);
574
629
 
575
630
  throw _err("unexpected character '" + c + "'", "toml/expected-value");
576
631
  }
@@ -53,6 +53,7 @@ var C = require("../constants");
53
53
  var pick = require("../pick");
54
54
  var numericBounds = require("../numeric-bounds");
55
55
  var safeBuffer = require("../safe-buffer");
56
+ var codepointClass = require("../codepoint-class");
56
57
  var { FrameworkError } = require("../framework-error");
57
58
 
58
59
  class SafeXmlError extends FrameworkError {
@@ -405,7 +406,12 @@ function parse(input, opts) {
405
406
  }
406
407
  }
407
408
  Object.assign(obj, grouped);
408
- var combinedText = textParts.join("").replace(/\s+/g, " ").trim();
409
+ // Collapse whitespace runs to a single space and trim, by walking rather
410
+ // than matching: the text came off the wire and a screen whose cost depends
411
+ // on the shape of the input is the thing this family does not do.
412
+ // splitOnWhitespace drops empty segments, so joining restores exactly the
413
+ // collapsed-and-trimmed form.
414
+ var combinedText = codepointClass.splitOnWhitespace(textParts.join("")).join(" ");
409
415
  if (combinedText.length > 0) obj["#text"] = combinedText;
410
416
  return _make(name, obj);
411
417
  }