From 578c0409281ecc13412ebbcc750c2e3e75c3a36f Mon Sep 17 00:00:00 2001 From: Theodor Steiner Date: Mon, 20 Feb 2023 20:47:33 +0900 Subject: [PATCH] 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. --- lib/internal/readline/interface.js | 28 +++++++---- lib/repl.js | 1 + .../test-repl-load-multiline-function.js | 47 +++++++++++++++++++ 3 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 test/parallel/test-repl-load-multiline-function.js diff --git a/lib/internal/readline/interface.js b/lib/internal/readline/interface.js index 45524612aea37b..75843fe96251ec 100644 --- a/lib/internal/readline/interface.js +++ b/lib/internal/readline/interface.js @@ -20,6 +20,7 @@ const { MathMaxApply, NumberIsFinite, ObjectSetPrototypeOf, + RegExp, RegExpPrototypeExec, StringPrototypeCodePointAt, StringPrototypeEndsWith, @@ -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'); @@ -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]) { @@ -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); } } } diff --git a/lib/repl.js b/lib/repl.js index 3875858871ebfb..638fb04689c3d0 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -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); diff --git a/test/parallel/test-repl-load-multiline-function.js b/test/parallel/test-repl-load-multiline-function.js new file mode 100644 index 00000000000000..055cd1cf2fd954 --- /dev/null +++ b/test/parallel/test-repl-load-multiline-function.js @@ -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();