Skip to content

Commit

Permalink
repl: fix .load infinite loop caused by shared use of lineEnding RegExp
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Theo-Steiner committed Feb 22, 2023
1 parent 1b87cb6 commit 578c040
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 10 deletions.
28 changes: 18 additions & 10 deletions lib/internal/readline/interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const {
MathMaxApply,
NumberIsFinite,
ObjectSetPrototypeOf,
RegExp,
RegExpPrototypeExec,
StringPrototypeCodePointAt,
StringPrototypeEndsWith,
Expand Down Expand Up @@ -72,7 +73,7 @@ const kHistorySize = 30;
const kMaxUndoRedoStackSize = 2048;
const kMincrlfDelay = 100;
// \r\n, \n, or \r followed by something other than \n
const lineEnding = /\r?\n|\r(?!\n)/g;
const lineEndingPattern = '\r?\n|\r(?!\n)';

const kLineObjectStream = Symbol('line object stream');
const kQuestionCancel = Symbol('kQuestionCancel');
Expand Down Expand Up @@ -585,6 +586,7 @@ class Interface extends InterfaceConstructor {
}

// Run test() on the new string chunk, not on the entire line buffer.
const lineEnding = new RegExp(lineEndingPattern, 'g');
let newPartContainsEnding = RegExpPrototypeExec(lineEnding, string);
if (newPartContainsEnding !== null) {
if (this[kLine_buffer]) {
Expand Down Expand Up @@ -1322,18 +1324,24 @@ class Interface extends InterfaceConstructor {
// falls through
default:
if (typeof s === 'string' && s) {
/**
* Use Regular Expression scoped to this block, as lastIndex and the state for RegExpPrototypeExec
* will be overwritten if the same RegEx instance is reused in recursive function calls.
*/
const lineEnding = new RegExp(lineEndingPattern, 'g');
let nextMatch = RegExpPrototypeExec(lineEnding, s);
if (nextMatch !== null) {
this[kInsertString](StringPrototypeSlice(s, 0, nextMatch.index));
let { lastIndex } = lineEnding;
while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null) {
this[kLine]();

// If no line endings are found, just insert the string as is
if (nextMatch === null) {
this[kInsertString](s);
} else {
// Keep track of the end of the last match
let lastIndex = 0;
do {
this[kInsertString](StringPrototypeSlice(s, lastIndex, nextMatch.index));
this[kLine]();
({ lastIndex } = lineEnding);
}
if (lastIndex === s.length) this[kLine]();
} else {
this[kInsertString](s);
} while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,7 @@ function REPLServer(prompt,
// code alignment
const matches = self._sawKeyPress ?
RegExpPrototypeExec(/^\s+/, cmd) : null;
// Preserve indentation in editorMode
if (matches) {
const prefix = matches[0];
self.write(prefix);
Expand Down
47 changes: 47 additions & 0 deletions test/parallel/test-repl-load-multiline-function.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict';
const common = require('../common');
const ArrayStream = require('../common/arraystream');
const assert = require('assert');
const join = require('path').join;
const fs = require('fs');

common.skipIfDumbTerminal();

const tmpdir = require('../common/tmpdir');
tmpdir.refresh();

const terminalCode = '(\u001b[1G\u001b[0J \u001b[1G)';
const terminalCodeRegex = new RegExp(terminalCode.replace(/\[/g, '\\['), 'g');

const repl = require('repl');

const inputStream = new ArrayStream();
const outputStream = new ArrayStream();

const r = repl.start({
prompt: '',
input: inputStream,
output: outputStream,
terminal: true,
useColors: false
});

const testFile = 'function a(b) {\n return b }\na(1)\n';
const testFileName = join(tmpdir.path, 'foo.js');
fs.writeFileSync(testFileName, testFile);

const command = `.load ${testFileName}\n`;
let accum = '';
outputStream.write = (data) => accum += data.replace('\r', '');


r.write(command);

const expected = command +
'function a(b) {\n' +
' return b }\n' +
'a(1)\n' +
'\n' +
'1\n';
assert.strictEqual(accum.replace(terminalCodeRegex, ''), expected);
r.close();

0 comments on commit 578c040

Please sign in to comment.