Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

repl: refactor to avoid unsafe array iteration #37188

Merged
merged 1 commit into from
Feb 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/readline.js
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ Interface.prototype._tabComplete = function(lastKeypressWasTab) {
}

// Result and the text that was completed.
const [completions, completeOn] = value;
const { 0: completions, 1: completeOn } = value;

if (!completions || completions.length === 0) {
return;
Expand Down
22 changes: 11 additions & 11 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const {
ArrayPrototypePush,
ArrayPrototypeReverse,
ArrayPrototypeShift,
ArrayPrototypeSome,
ArrayPrototypeSort,
ArrayPrototypeSplice,
ArrayPrototypeUnshift,
Expand Down Expand Up @@ -1155,7 +1156,7 @@ REPLServer.prototype.complete = function() {

function gracefulReaddir(...args) {
try {
return fs.readdirSync(...args);
return ReflectApply(fs.readdirSync, null, args);
} catch {}
}

Expand Down Expand Up @@ -1236,11 +1237,11 @@ function complete(line, callback) {
ArrayPrototypeForEach(paths, (dir) => {
dir = path.resolve(dir, subdir);
const dirents = gracefulReaddir(dir, { withFileTypes: true }) || [];
for (const dirent of dirents) {
ArrayPrototypeForEach(dirents, (dirent) => {
if (RegExpPrototypeTest(versionedFileNamesRe, dirent.name) ||
dirent.name === '.npm') {
// Exclude versioned names that 'npm' installs.
continue;
return;
}
const extension = path.extname(dirent.name);
const base = StringPrototypeSlice(dirent.name, 0, -extension.length);
Expand All @@ -1249,18 +1250,17 @@ function complete(line, callback) {
(!subdir || base !== 'index')) {
ArrayPrototypePush(group, `${subdir}${base}`);
}
continue;
return;
}
ArrayPrototypePush(group, `${subdir}${dirent.name}/`);
const absolute = path.resolve(dir, dirent.name);
const subfiles = gracefulReaddir(absolute) || [];
for (const subfile of subfiles) {
if (ArrayPrototypeIncludes(indexes, subfile)) {
ArrayPrototypePush(group, `${subdir}${dirent.name}`);
break;
}
if (ArrayPrototypeSome(
gracefulReaddir(absolute) || [],
(subfile) => ArrayPrototypeIncludes(indexes, subfile)
)) {
ArrayPrototypePush(group, `${subdir}${dirent.name}`);
}
}
});
});
if (group.length) {
ArrayPrototypePush(completionGroups, group);
Expand Down
215 changes: 215 additions & 0 deletions test/parallel/test-repl-autocomplete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
'use strict';

// Flags: --expose-internals

const common = require('../common');
const stream = require('stream');
const REPL = require('internal/repl');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { inspect } = require('util');

common.skipIfDumbTerminal();

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

process.throwDeprecation = true;

const defaultHistoryPath = path.join(tmpdir.path, '.node_repl_history');

// Create an input stream specialized for testing an array of actions
class ActionStream extends stream.Stream {
run(data) {
const _iter = data[Symbol.iterator]();
const doAction = () => {
const next = _iter.next();
if (next.done) {
// Close the repl. Note that it must have a clean prompt to do so.
this.emit('keypress', '', { ctrl: true, name: 'd' });
return;
}
const action = next.value;

if (typeof action === 'object') {
this.emit('keypress', '', action);
} else {
this.emit('data', `${action}`);
}
setImmediate(doAction);
};
doAction();
}
resume() {}
pause() {}
}
ActionStream.prototype.readable = true;

// Mock keys
const ENTER = { name: 'enter' };
const UP = { name: 'up' };
const DOWN = { name: 'down' };
const LEFT = { name: 'left' };
const RIGHT = { name: 'right' };
const BACKSPACE = { name: 'backspace' };
const TABULATION = { name: 'tab' };
const WORD_LEFT = { name: 'left', ctrl: true };
const WORD_RIGHT = { name: 'right', ctrl: true };
const GO_TO_END = { name: 'end' };
const SIGINT = { name: 'c', ctrl: true };
const ESCAPE = { name: 'escape', meta: true };

const prompt = '> ';

const tests = [
{
env: { NODE_REPL_HISTORY: defaultHistoryPath },
test: (function*() {
// Deleting Array iterator should not break history feature.
//
// Using a generator function instead of an object to allow the test to
// keep iterating even when Array.prototype[Symbol.iterator] has been
// deleted.
yield 'const ArrayIteratorPrototype =';
yield ' Object.getPrototypeOf(Array.prototype[Symbol.iterator]());';
yield ENTER;
yield 'const {next} = ArrayIteratorPrototype;';
yield ENTER;
yield 'const realArrayIterator = Array.prototype[Symbol.iterator];';
yield ENTER;
yield 'delete Array.prototype[Symbol.iterator];';
yield ENTER;
yield 'delete ArrayIteratorPrototype.next;';
yield ENTER;
yield UP;
yield UP;
yield DOWN;
yield DOWN;
yield 'fu';
yield 'n';
yield RIGHT;
yield BACKSPACE;
yield LEFT;
yield LEFT;
yield 'A';
yield BACKSPACE;
yield GO_TO_END;
yield BACKSPACE;
yield WORD_LEFT;
yield WORD_RIGHT;
yield ESCAPE;
yield ENTER;
yield 'require("./';
yield TABULATION;
yield SIGINT;
yield 'Array.proto';
yield RIGHT;
yield '.pu';
yield ENTER;
yield 'ArrayIteratorPrototype.next = next;';
yield ENTER;
yield 'Array.prototype[Symbol.iterator] = realArrayIterator;';
yield ENTER;
})(),
expected: [],
clean: false
},
];
const numtests = tests.length;

const runTestWrap = common.mustCall(runTest, numtests);

function cleanupTmpFile() {
try {
// Write over the file, clearing any history
fs.writeFileSync(defaultHistoryPath, '');
} catch (err) {
if (err.code === 'ENOENT') return true;
throw err;
}
return true;
}

function runTest() {
const opts = tests.shift();
if (!opts) return; // All done

const { expected, skip } = opts;

// Test unsupported on platform.
if (skip) {
setImmediate(runTestWrap, true);
return;
}
const lastChunks = [];
let i = 0;

REPL.createInternalRepl(opts.env, {
input: new ActionStream(),
output: new stream.Writable({
write(chunk, _, next) {
const output = chunk.toString();

if (!opts.showEscapeCodes &&
(output[0] === '\x1B' || /^[\r\n]+$/.test(output))) {
return next();
}

lastChunks.push(output);

if (expected.length && !opts.checkTotal) {
try {
assert.strictEqual(output, expected[i]);
} catch (e) {
console.error(`Failed test # ${numtests - tests.length}`);
console.error('Last outputs: ' + inspect(lastChunks, {
breakLength: 5, colors: true
}));
throw e;
}
// TODO(BridgeAR): Auto close on last chunk!
i++;
}

next();
}
}),
allowBlockingCompletions: true,
completer: opts.completer,
prompt,
useColors: false,
preview: opts.preview,
terminal: true
}, function(err, repl) {
if (err) {
console.error(`Failed test # ${numtests - tests.length}`);
throw err;
}

repl.once('close', () => {
if (opts.clean)
cleanupTmpFile();

if (opts.checkTotal) {
assert.deepStrictEqual(lastChunks, expected);
} else if (expected.length !== i) {
console.error(tests[numtests - tests.length - 1]);
throw new Error(`Failed test # ${numtests - tests.length}`);
}

setImmediate(runTestWrap, true);
});

if (opts.columns) {
Object.defineProperty(repl, 'columns', {
value: opts.columns,
enumerable: true
});
}
repl.input.run(opts.test);
});
}

// run the tests
runTest();