Skip to content

Commit f23e74b

Browse files
committed
fixup! repl: refactor to avoid unsafe array iteration
1 parent 5afd6bf commit f23e74b

File tree

2 files changed

+215
-5
lines changed

2 files changed

+215
-5
lines changed
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
'use strict';
2+
3+
// Flags: --expose-internals
4+
5+
const common = require('../common');
6+
const stream = require('stream');
7+
const REPL = require('internal/repl');
8+
const assert = require('assert');
9+
const fs = require('fs');
10+
const path = require('path');
11+
const { inspect } = require('util');
12+
13+
common.skipIfDumbTerminal();
14+
15+
const tmpdir = require('../common/tmpdir');
16+
tmpdir.refresh();
17+
18+
process.throwDeprecation = true;
19+
20+
const defaultHistoryPath = path.join(tmpdir.path, '.node_repl_history');
21+
22+
// Create an input stream specialized for testing an array of actions
23+
class ActionStream extends stream.Stream {
24+
run(data) {
25+
const _iter = data[Symbol.iterator]();
26+
const doAction = () => {
27+
const next = _iter.next();
28+
if (next.done) {
29+
// Close the repl. Note that it must have a clean prompt to do so.
30+
this.emit('keypress', '', { ctrl: true, name: 'd' });
31+
return;
32+
}
33+
const action = next.value;
34+
35+
if (typeof action === 'object') {
36+
this.emit('keypress', '', action);
37+
} else {
38+
this.emit('data', `${action}`);
39+
}
40+
setImmediate(doAction);
41+
};
42+
doAction();
43+
}
44+
resume() {}
45+
pause() {}
46+
}
47+
ActionStream.prototype.readable = true;
48+
49+
// Mock keys
50+
const ENTER = { name: 'enter' };
51+
const UP = { name: 'up' };
52+
const DOWN = { name: 'down' };
53+
const LEFT = { name: 'left' };
54+
const RIGHT = { name: 'right' };
55+
const BACKSPACE = { name: 'backspace' };
56+
const TABULATION = { name: 'tab' };
57+
const WORD_LEFT = { name: 'left', ctrl: true };
58+
const WORD_RIGHT = { name: 'right', ctrl: true };
59+
const GO_TO_END = { name: 'end' };
60+
const SIGINT = { name: 'c', ctrl: true };
61+
const ESCAPE = { name: 'escape', meta: true };
62+
63+
const prompt = '> ';
64+
65+
const tests = [
66+
{
67+
env: { NODE_REPL_HISTORY: defaultHistoryPath },
68+
test: (function*() {
69+
// Deleting Array iterator should not break history feature.
70+
//
71+
// Using a generator function instead of an object to allow the test to
72+
// keep iterating even when Array.prototype[Symbol.iterator] has been
73+
// deleted.
74+
yield 'const ArrayIteratorPrototype =';
75+
yield ' Object.getPrototypeOf(Array.prototype[Symbol.iterator]());';
76+
yield ENTER;
77+
yield 'const {next} = ArrayIteratorPrototype;';
78+
yield ENTER;
79+
yield 'const realArrayIterator = Array.prototype[Symbol.iterator];';
80+
yield ENTER;
81+
yield 'delete Array.prototype[Symbol.iterator];';
82+
yield ENTER;
83+
yield 'delete ArrayIteratorPrototype.next;';
84+
yield ENTER;
85+
yield UP;
86+
yield UP;
87+
yield DOWN;
88+
yield DOWN;
89+
yield 'fu';
90+
yield 'n';
91+
yield RIGHT;
92+
yield BACKSPACE;
93+
yield LEFT;
94+
yield LEFT;
95+
yield 'A';
96+
yield BACKSPACE;
97+
yield GO_TO_END;
98+
yield BACKSPACE;
99+
yield WORD_LEFT;
100+
yield WORD_RIGHT;
101+
yield ESCAPE;
102+
yield ENTER;
103+
yield 'require("./';
104+
yield TABULATION;
105+
yield SIGINT;
106+
yield 'Array.proto';
107+
yield RIGHT;
108+
yield '.pu';
109+
yield ENTER;
110+
yield 'ArrayIteratorPrototype.next = next;';
111+
yield ENTER;
112+
yield 'Array.prototype[Symbol.iterator] = realArrayIterator;';
113+
yield ENTER;
114+
})(),
115+
expected: [],
116+
clean: false
117+
},
118+
];
119+
const numtests = tests.length;
120+
121+
const runTestWrap = common.mustCall(runTest, numtests);
122+
123+
function cleanupTmpFile() {
124+
try {
125+
// Write over the file, clearing any history
126+
fs.writeFileSync(defaultHistoryPath, '');
127+
} catch (err) {
128+
if (err.code === 'ENOENT') return true;
129+
throw err;
130+
}
131+
return true;
132+
}
133+
134+
function runTest() {
135+
const opts = tests.shift();
136+
if (!opts) return; // All done
137+
138+
const { expected, skip } = opts;
139+
140+
// Test unsupported on platform.
141+
if (skip) {
142+
setImmediate(runTestWrap, true);
143+
return;
144+
}
145+
const lastChunks = [];
146+
let i = 0;
147+
148+
REPL.createInternalRepl(opts.env, {
149+
input: new ActionStream(),
150+
output: new stream.Writable({
151+
write(chunk, _, next) {
152+
const output = chunk.toString();
153+
154+
if (!opts.showEscapeCodes &&
155+
(output[0] === '\x1B' || /^[\r\n]+$/.test(output))) {
156+
return next();
157+
}
158+
159+
lastChunks.push(output);
160+
161+
if (expected.length && !opts.checkTotal) {
162+
try {
163+
assert.strictEqual(output, expected[i]);
164+
} catch (e) {
165+
console.error(`Failed test # ${numtests - tests.length}`);
166+
console.error('Last outputs: ' + inspect(lastChunks, {
167+
breakLength: 5, colors: true
168+
}));
169+
throw e;
170+
}
171+
// TODO(BridgeAR): Auto close on last chunk!
172+
i++;
173+
}
174+
175+
next();
176+
}
177+
}),
178+
allowBlockingCompletions: true,
179+
completer: opts.completer,
180+
prompt,
181+
useColors: false,
182+
preview: opts.preview,
183+
terminal: true
184+
}, function(err, repl) {
185+
if (err) {
186+
console.error(`Failed test # ${numtests - tests.length}`);
187+
throw err;
188+
}
189+
190+
repl.once('close', () => {
191+
if (opts.clean)
192+
cleanupTmpFile();
193+
194+
if (opts.checkTotal) {
195+
assert.deepStrictEqual(lastChunks, expected);
196+
} else if (expected.length !== i) {
197+
console.error(tests[numtests - tests.length - 1]);
198+
throw new Error(`Failed test # ${numtests - tests.length}`);
199+
}
200+
201+
setImmediate(runTestWrap, true);
202+
});
203+
204+
if (opts.columns) {
205+
Object.defineProperty(repl, 'columns', {
206+
value: opts.columns,
207+
enumerable: true
208+
});
209+
}
210+
repl.input.run(opts.test);
211+
});
212+
}
213+
214+
// run the tests
215+
runTest();

test/parallel/test-repl-history-navigation.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ const LEFT = { name: 'left' };
5454
const RIGHT = { name: 'right' };
5555
const DELETE = { name: 'delete' };
5656
const BACKSPACE = { name: 'backspace' };
57-
const TABULATION = { name: 'tab' };
5857
const WORD_LEFT = { name: 'left', ctrl: true };
5958
const WORD_RIGHT = { name: 'right', ctrl: true };
6059
const GO_TO_END = { name: 'end' };
@@ -543,9 +542,6 @@ const tests = [
543542
yield WORD_RIGHT;
544543
yield ESCAPE;
545544
yield ENTER;
546-
yield 'require("./';
547-
yield TABULATION;
548-
yield SIGINT;
549545
yield 'Array.proto';
550546
yield RIGHT;
551547
yield '.pu';
@@ -618,7 +614,6 @@ function runTest() {
618614
next();
619615
}
620616
}),
621-
allowBlockingCompletions: true,
622617
completer: opts.completer,
623618
prompt,
624619
useColors: false,

0 commit comments

Comments
 (0)