Skip to content

Commit 01594eb

Browse files
committed
add infinite loop protection
1 parent 1df7cff commit 01594eb

File tree

7 files changed

+1186
-127
lines changed

7 files changed

+1186
-127
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@codemirror/theme-one-dark": "^6.1.2",
2525
"@litecanvas/plugin-asset-loader": "latest",
2626
"@litecanvas/utils": "latest",
27+
"babel-standalone": "^6.26.0",
2728
"codemirror": "^6.0.1",
2829
"eslint-linter-browserify": "9.20.1",
2930
"litecanvas": "latest",

public/app.js

+1,056-124
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/preview.html

+5
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@
6565
handleError = (e) => {
6666
if (errorMessage) return;
6767
errorMessage = e.message || e.reason;
68+
if (errorMessage.includes("Potential infinite loop")) {
69+
errorMessage = "Error: Potential infinite loop";
70+
window["err-tip"].textContent =
71+
"Tip: Check your code loops (for, while, etc)";
72+
}
6873
console.error(errorMessage);
6974
err.prepend(errorMessage);
7075
err.style.display = "block";

public/sw.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const cacheName = "luizbills.litecanvas-editor-v1";
2-
const version = "2.58.0";
2+
const version = "2.59.0";
33

44
const precacheResources = [
55
"/",

src/index.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import { indentWithTab } from "@codemirror/commands";
1212
import { oneDark } from "@codemirror/theme-one-dark";
1313
import * as eslint from "eslint-linter-browserify";
1414

15+
import { show, hide, $, prepareCode } from "./utils";
1516
import editorSetup from "./editorSetup";
16-
import { show, hide, $ } from "./utils";
1717
import demo from "./demo";
1818
import customCompletions from "./completions";
1919
import mobileBar from "./mobileBar";
@@ -157,7 +157,8 @@ function runCode() {
157157

158158
function loadCode() {
159159
const code = window.codeEditor.state.doc.toString();
160-
getIframe().contentDocument.querySelector("#code").innerHTML = code;
160+
getIframe().contentDocument.querySelector("#code").innerHTML =
161+
prepareCode(code);
161162
}
162163

163164
/**

src/loop-protect.js

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* Based on https://github.com/jsbin/loop-protect/blob/master/lib/index.js | License: http://jsbin.mit-license.org
3+
*/
4+
const generateBefore = (t, id) =>
5+
t.variableDeclaration("let", [
6+
t.variableDeclarator(
7+
id,
8+
t.callExpression(
9+
t.memberExpression(t.identifier("Date"), t.identifier("now")),
10+
[]
11+
)
12+
),
13+
]);
14+
15+
const generateInside = ({ t, id, line, ch, timeout, extra } = {}) => {
16+
return t.ifStatement(
17+
t.binaryExpression(
18+
">",
19+
t.binaryExpression(
20+
"-",
21+
t.callExpression(
22+
t.memberExpression(t.identifier("Date"), t.identifier("now")),
23+
[]
24+
),
25+
id
26+
),
27+
t.numericLiteral(timeout)
28+
),
29+
t.throwStatement(t.stringLiteral("Potential infinite loop"))
30+
);
31+
};
32+
33+
const protect = (t, timeout, extra) => (path) => {
34+
if (!path.node.loc) {
35+
return;
36+
}
37+
const id = path.scope.generateUidIdentifier(randomString(16));
38+
const before = generateBefore(t, id);
39+
const inside = generateInside({
40+
t,
41+
id,
42+
line: path.node.loc.start.line,
43+
ch: path.node.loc.start.column,
44+
timeout,
45+
extra,
46+
});
47+
const body = path.get("body");
48+
49+
// if we have an expression statement, convert it to a block
50+
if (!t.isBlockStatement(body)) {
51+
body.replaceWith(t.blockStatement([body.node]));
52+
}
53+
path.insertBefore(before);
54+
body.unshiftContainer("body", inside);
55+
};
56+
57+
export default (timeout = 100, extra = null) => {
58+
if (typeof extra === "string") {
59+
const string = extra;
60+
extra = `() => console.error("${string.replace(/"/g, '\\"')}")`;
61+
} else if (extra !== null) {
62+
extra = extra.toString();
63+
if (extra.startsWith("function (")) {
64+
// fix anonymous functions as they'll cause
65+
// the callback transform to blow up
66+
extra = extra.replace(/^function \(/, "function callback(");
67+
}
68+
}
69+
70+
return ({ types: t, transform }) => {
71+
const node = extra
72+
? transform(extra, { ast: true }).ast.program.body[0]
73+
: null;
74+
75+
let callback = null;
76+
if (t.isExpressionStatement(node)) {
77+
callback = node.expression;
78+
} else if (t.isFunctionDeclaration(node)) {
79+
callback = t.functionExpression(null, node.params, node.body);
80+
}
81+
82+
return {
83+
visitor: {
84+
WhileStatement: protect(t, timeout, callback),
85+
ForStatement: protect(t, timeout, callback),
86+
DoWhileStatement: protect(t, timeout, callback),
87+
},
88+
};
89+
};
90+
};
91+
92+
function randomString(length) {
93+
const result = [];
94+
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
95+
const charactersLength = characters.length;
96+
let counter = 0;
97+
while (counter < length) {
98+
result.push(
99+
characters.charAt(Math.floor(Math.random() * charactersLength))
100+
);
101+
counter += 1;
102+
}
103+
return result.join("");
104+
}

src/utils.js

+16
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import Babel from "babel-standalone";
2+
import loopProtection from "./loop-protect";
3+
14
/**
25
* @param {HTMLElement} el
36
*/
@@ -31,3 +34,16 @@ export function $(selector, parent = document) {
3134
export function $$(selector, parent = document) {
3235
return parent.querySelectorAll(selector);
3336
}
37+
38+
/**
39+
* @param {string} code
40+
* @returns {string}
41+
*/
42+
export function prepareCode(code) {
43+
const timeout = 500;
44+
Babel.registerPlugin("loopProtection", loopProtection(timeout));
45+
46+
return Babel.transform(code, {
47+
plugins: ["loopProtection"],
48+
}).code;
49+
}

0 commit comments

Comments
 (0)