Skip to content

Commit 578c040

Browse files
committed
repl: fix .load infinite loop caused by shared use of lineEnding RegExp
Since the lineEnding Regular Expression is declared on the module scope, recursive invocations of its `[kTtyWrite]` method share one instance of this Regular Expression. Since the state of a RegExp is managed by instance, alternately calling RegExpPrototypeExec with the same RegExp on different strings can lead to the state changing unexpectedly. This is the root cause of this infinite loop bug when calling .load on javascript files of certain shapes.
1 parent 1b87cb6 commit 578c040

File tree

3 files changed

+66
-10
lines changed

3 files changed

+66
-10
lines changed

lib/internal/readline/interface.js

+18-10
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const {
2020
MathMaxApply,
2121
NumberIsFinite,
2222
ObjectSetPrototypeOf,
23+
RegExp,
2324
RegExpPrototypeExec,
2425
StringPrototypeCodePointAt,
2526
StringPrototypeEndsWith,
@@ -72,7 +73,7 @@ const kHistorySize = 30;
7273
const kMaxUndoRedoStackSize = 2048;
7374
const kMincrlfDelay = 100;
7475
// \r\n, \n, or \r followed by something other than \n
75-
const lineEnding = /\r?\n|\r(?!\n)/g;
76+
const lineEndingPattern = '\r?\n|\r(?!\n)';
7677

7778
const kLineObjectStream = Symbol('line object stream');
7879
const kQuestionCancel = Symbol('kQuestionCancel');
@@ -585,6 +586,7 @@ class Interface extends InterfaceConstructor {
585586
}
586587

587588
// Run test() on the new string chunk, not on the entire line buffer.
589+
const lineEnding = new RegExp(lineEndingPattern, 'g');
588590
let newPartContainsEnding = RegExpPrototypeExec(lineEnding, string);
589591
if (newPartContainsEnding !== null) {
590592
if (this[kLine_buffer]) {
@@ -1322,18 +1324,24 @@ class Interface extends InterfaceConstructor {
13221324
// falls through
13231325
default:
13241326
if (typeof s === 'string' && s) {
1327+
/**
1328+
* Use Regular Expression scoped to this block, as lastIndex and the state for RegExpPrototypeExec
1329+
* will be overwritten if the same RegEx instance is reused in recursive function calls.
1330+
*/
1331+
const lineEnding = new RegExp(lineEndingPattern, 'g');
13251332
let nextMatch = RegExpPrototypeExec(lineEnding, s);
1326-
if (nextMatch !== null) {
1327-
this[kInsertString](StringPrototypeSlice(s, 0, nextMatch.index));
1328-
let { lastIndex } = lineEnding;
1329-
while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null) {
1330-
this[kLine]();
1333+
1334+
// If no line endings are found, just insert the string as is
1335+
if (nextMatch === null) {
1336+
this[kInsertString](s);
1337+
} else {
1338+
// Keep track of the end of the last match
1339+
let lastIndex = 0;
1340+
do {
13311341
this[kInsertString](StringPrototypeSlice(s, lastIndex, nextMatch.index));
1342+
this[kLine]();
13321343
({ lastIndex } = lineEnding);
1333-
}
1334-
if (lastIndex === s.length) this[kLine]();
1335-
} else {
1336-
this[kInsertString](s);
1344+
} while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null);
13371345
}
13381346
}
13391347
}

lib/repl.js

+1
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,7 @@ function REPLServer(prompt,
861861
// code alignment
862862
const matches = self._sawKeyPress ?
863863
RegExpPrototypeExec(/^\s+/, cmd) : null;
864+
// Preserve indentation in editorMode
864865
if (matches) {
865866
const prefix = matches[0];
866867
self.write(prefix);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict';
2+
const common = require('../common');
3+
const ArrayStream = require('../common/arraystream');
4+
const assert = require('assert');
5+
const join = require('path').join;
6+
const fs = require('fs');
7+
8+
common.skipIfDumbTerminal();
9+
10+
const tmpdir = require('../common/tmpdir');
11+
tmpdir.refresh();
12+
13+
const terminalCode = '(\u001b[1G\u001b[0J \u001b[1G)';
14+
const terminalCodeRegex = new RegExp(terminalCode.replace(/\[/g, '\\['), 'g');
15+
16+
const repl = require('repl');
17+
18+
const inputStream = new ArrayStream();
19+
const outputStream = new ArrayStream();
20+
21+
const r = repl.start({
22+
prompt: '',
23+
input: inputStream,
24+
output: outputStream,
25+
terminal: true,
26+
useColors: false
27+
});
28+
29+
const testFile = 'function a(b) {\n return b }\na(1)\n';
30+
const testFileName = join(tmpdir.path, 'foo.js');
31+
fs.writeFileSync(testFileName, testFile);
32+
33+
const command = `.load ${testFileName}\n`;
34+
let accum = '';
35+
outputStream.write = (data) => accum += data.replace('\r', '');
36+
37+
38+
r.write(command);
39+
40+
const expected = command +
41+
'function a(b) {\n' +
42+
' return b }\n' +
43+
'a(1)\n' +
44+
'\n' +
45+
'1\n';
46+
assert.strictEqual(accum.replace(terminalCodeRegex, ''), expected);
47+
r.close();

0 commit comments

Comments
 (0)