Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| if (typeof DEBUG === 'undefined') { DEBUG = true; } | |
| (function (root, factory) { | |
| 'use strict'; | |
| /*global define*/ | |
| if (typeof define === 'function' && define.amd) { | |
| define(factory(root)); | |
| } else if (typeof exports === 'object') { | |
| module.exports = factory(root); | |
| } else { | |
| root.loopProtect = factory(root); | |
| } | |
| })(this, function loopProtectModule(root) { | |
| /*global DEBUG*/ | |
| 'use strict'; | |
| var debug = null; | |
| // the standard loops - note that recursive is not supported | |
| var re = /\b(for|while|do)\b/g; | |
| var reSingle = /\b(for|while|do)\b/; | |
| var labelRe = /\b(?!default:)([a-z_]{1}\w+:)/i; | |
| var comments = /(?:\/\*(?:[\s\S]*?)\*\/)|(?:([\s;])+\/\/(?:.*)$)/gm; | |
| var loopTimeout = 1000; | |
| var loopProtect = rewriteLoops; | |
| // used in the loop detection | |
| loopProtect.counters = {}; | |
| // expose debug info | |
| loopProtect.debug = function debugSwitch(state) { | |
| debug = state ? function () { | |
| console.log.apply(console, [].slice.apply(arguments)); | |
| } : function () {}; | |
| }; | |
| loopProtect.debug(false); // off by default | |
| // the method - as this could be aliased to something else | |
| loopProtect.alias = 'loopProtect'; | |
| function inMultilineComment(lineNum, lines) { | |
| if (lineNum === 0) { | |
| return false; | |
| } | |
| var j = lineNum; | |
| var closeCommentTags = 1; // let's assume we're inside a comment | |
| var closePos = -1; | |
| var openPos = -1; | |
| do { | |
| DEBUG && debug('looking backwards ' + lines[j]); // jshint ignore:line | |
| closePos = lines[j].indexOf('*/'); | |
| openPos = lines[j].indexOf('/*'); | |
| if (closePos !== -1) { | |
| closeCommentTags++; | |
| } | |
| // check for single line /* comment */ formatted comments | |
| if (closePos === lines[j].length - 2 && openPos !== -1) { | |
| closeCommentTags--; | |
| } | |
| if (openPos !== -1) { | |
| closeCommentTags--; | |
| if (closeCommentTags === 0) { | |
| DEBUG && debug('- exit: part of a multiline comment'); // jshint ignore:line | |
| return true; | |
| } | |
| } | |
| j -= 1; | |
| } while (j !== 0); | |
| return false; | |
| } | |
| function inCommentOrString(index, line) { | |
| var character; | |
| while (--index > -1) { | |
| character = line.substr(index, 1); | |
| if (character === '"' || character === '\'' || character === '.') { | |
| // our loop keyword was actually either in a string or a property, so let's exit and ignore this line | |
| DEBUG && debug('- exit: matched inside a string or property key'); // jshint ignore:line | |
| return true; | |
| } | |
| if (character === '/' || character === '*') { | |
| // looks like a comment, go back one to confirm or not | |
| --index; | |
| if (character === '/') { | |
| // we've found a comment, so let's exit and ignore this line | |
| DEBUG && debug('- exit: part of a comment'); // jshint ignore:line | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| function directlyBeforeLoop(index, lineNum, lines) { | |
| reSingle.lastIndex = 0; | |
| labelRe.lastIndex = 0; | |
| var beforeLoop = false; | |
| var theRest = lines.slice(lineNum).join('\n').substr(index).replace(labelRe, ''); | |
| theRest.replace(reSingle, function commentStripper(match, capture, i) { | |
| var target = theRest.substr(0, i).replace(comments, '').trim(); | |
| DEBUG && debug('- directlyBeforeLoop: ' + target); // jshint ignore:line | |
| if (target.length === 0) { | |
| beforeLoop = true; | |
| } | |
| // strip comments out of the target, and if there's nothing else | |
| // it's a valid label...I hope! | |
| }); | |
| return beforeLoop; | |
| } | |
| /** | |
| * Look for for, while and do loops, and inserts *just* at the start of the | |
| * loop, a check function. | |
| */ | |
| function rewriteLoops(code, offset) { | |
| var recompiled = []; | |
| var lines = code.split('\n'); | |
| var disableLoopProtection = false; | |
| var method = loopProtect.alias + '.protect'; | |
| var ignore = {}; | |
| var pushonly = {}; | |
| var labelPostion = null; | |
| function insertReset(lineNum, line, matchPosition) { | |
| // recompile the line with the reset **just** before the actual loop | |
| // so that we insert in to the correct location (instead of possibly | |
| // outside the logic | |
| return line.slice(0, matchPosition) + ';' + method + '({ line: ' + lineNum + ', reset: true }); ' + line.slice(matchPosition); | |
| } | |
| if (!offset) { | |
| offset = 0; | |
| } | |
| lines.forEach(function eachLine(line, lineNum) { | |
| // reset our regexp each time. | |
| re.lastIndex = 0; | |
| labelRe.lastIndex = 0; | |
| if (disableLoopProtection) { | |
| return; | |
| } | |
| if (line.toLowerCase().indexOf('noprotect') !== -1) { | |
| disableLoopProtection = true; | |
| } | |
| var index = -1; | |
| var matchPosition = -1; | |
| var originalLineNum = lineNum; | |
| // +1 since we're humans and don't read lines numbers from zero | |
| var printLineNumber = lineNum - offset + 1; | |
| var character = ''; | |
| // special case for `do` loops, as they're end with `while` | |
| var dofound = false; | |
| var findwhile = false; | |
| var terminator = false; | |
| var matches = line.match(re) || []; | |
| var match = matches.length ? matches[0] : ''; | |
| var labelMatch = line.match(labelRe) || []; | |
| var openBrackets = 0; | |
| var openBraces = 0; | |
| if (labelMatch.length) { | |
| DEBUG && debug('- label match'); // jshint ignore:line | |
| index = line.indexOf(labelMatch[1]); | |
| if (!inCommentOrString(index, line)) { | |
| if (!inMultilineComment(lineNum, lines)) { | |
| if (directlyBeforeLoop(index, lineNum, lines)) { | |
| DEBUG && debug('- found a label: "' + labelMatch[0] + '"'); // jshint ignore:line | |
| labelPostion = lineNum; | |
| } else { | |
| DEBUG && debug('- ignored "label", false positive'); // jshint ignore:line | |
| } | |
| } else { | |
| DEBUG && debug('- ignored label in multline comment'); // jshint ignore:line | |
| } | |
| } else { | |
| DEBUG && debug('- ignored label in string or comment'); // jshint ignore:line | |
| } | |
| } | |
| if (ignore[lineNum]) { | |
| DEBUG && debug(' -exit: ignoring line ' + lineNum +': ' + line); // jshint ignore:line | |
| return; | |
| } | |
| if (pushonly[lineNum]) { | |
| DEBUG && debug('- exit: ignoring, but adding line ' + lineNum + ': ' + line); // jshint ignore:line | |
| recompiled.push(line); | |
| return; | |
| } | |
| // if there's more than one match, we just ignore this kind of loop | |
| // otherwise I'm going to be writing a full JavaScript lexer...and god | |
| // knows I've got better things to be doing. | |
| if (match && matches.length === 1 && line.indexOf('jsbin') === -1) { | |
| DEBUG && debug('match on ' + match + '\n'); // jshint ignore:line | |
| // there's a special case for protecting `do` loops, we need to first | |
| // prtect the `do`, but then ignore the closing `while` statement, so | |
| // we reset the search state for this special case. | |
| dofound = match === 'do'; | |
| // make sure this is an actual loop command by searching backwards | |
| // to ensure it's not a string, comment or object property | |
| matchPosition = index = line.indexOf(match); | |
| // first we need to walk backwards to ensure that our match isn't part | |
| // of a string or part of a comment | |
| if (inCommentOrString(index, line)) { | |
| recompiled.push(line); | |
| return; | |
| } | |
| // it's quite possible we're in the middle of a multiline | |
| // comment, so we'll cycle up looking for an opening comment, | |
| // and if there's one (and not a closing `*/`), then we'll | |
| // ignore this line as a comment | |
| if (inMultilineComment(lineNum, lines)) { | |
| recompiled.push(line); | |
| return; | |
| } | |
| // now work our way forward to look for '{' | |
| index = line.indexOf(match) + match.length; | |
| if (index === line.length) { | |
| if (index === line.length && lineNum < (lines.length-1)) { | |
| // move to the next line | |
| DEBUG && debug('- moving to next line'); // jshint ignore:line | |
| recompiled.push(line); | |
| lineNum++; | |
| line = lines[lineNum]; | |
| ignore[lineNum] = true; | |
| index = 0; | |
| } | |
| } | |
| while (index < line.length) { | |
| character = line.substr(index, 1); | |
| // DEBUG && debug(character, index); // jshint ignore:line | |
| if (character === '(') { | |
| openBrackets++; | |
| } | |
| if (character === ')') { | |
| openBrackets--; | |
| if (openBrackets === 0 && terminator === false) { | |
| terminator = index; | |
| } | |
| } | |
| if (character === '{') { | |
| openBraces++; | |
| } | |
| if (character === '}') { | |
| openBraces--; | |
| } | |
| if (openBrackets === 0 && (character === ';' || character === '{')) { | |
| // if we're a non-curlies loop, then convert to curlies to get our code inserted | |
| if (character === ';') { | |
| if (lineNum !== originalLineNum) { | |
| DEBUG && debug('- multiline inline loop'); // jshint ignore:line | |
| // affect the compiled line | |
| recompiled[originalLineNum] = recompiled[originalLineNum].substring(0, terminator + 1) + '{\nif (' + method + '({ line: ' + printLineNumber + ' })) break;\n' + recompiled[originalLineNum].substring(terminator + 1); | |
| line += '\n}\n'; | |
| } else { | |
| // simpler | |
| DEBUG && debug('- single line inline loop'); // jshint ignore:line | |
| line = line.substring(0, terminator + 1) + '{\nif (' + method + '({ line: ' + printLineNumber + ' })) break;\n' + line.substring(terminator + 1) + '\n}\n'; | |
| } | |
| } else if (character === '{') { | |
| DEBUG && debug('- multiline with braces'); // jshint ignore:line | |
| var insert = ';\nif (' + method + '({ line: ' + printLineNumber + ' })) break;\n'; | |
| line = line.substring(0, index + 1) + insert + line.substring(index + 1); | |
| index += insert.length; | |
| } | |
| // work out where to put the reset | |
| if (lineNum === originalLineNum && labelPostion === null) { | |
| DEBUG && debug('- simple reset insert'); // jshint ignore:line | |
| line = insertReset(printLineNumber, line, matchPosition); | |
| index += (';' + method + '({ line: ' + lineNum + ', reset: true }); ').length; | |
| } else { | |
| // insert the reset above the originalLineNum OR if this loop used | |
| // a label, we have to insert the reset *above* the label | |
| if (labelPostion === null) { | |
| DEBUG && debug('- reset inserted above original line'); // jshint ignore:line | |
| recompiled[originalLineNum] = insertReset(printLineNumber, recompiled[originalLineNum], matchPosition); | |
| } else { | |
| DEBUG && debug('- reset inserted above matched label on line ' + labelPostion); // jshint ignore:line | |
| if (recompiled[labelPostion] === undefined) { | |
| labelPostion--; | |
| matchPosition = 0; | |
| } | |
| recompiled[labelPostion] = insertReset(printLineNumber, recompiled[labelPostion], matchPosition); | |
| labelPostion = null; | |
| } | |
| } | |
| recompiled.push(line); | |
| if (!dofound) { | |
| return; | |
| } else { | |
| DEBUG && debug('searching for closing `while` statement for: ' + line); // jshint ignore:line | |
| // cycle forward until we find the close brace, after which should | |
| // be our while statement to ignore | |
| findwhile = false; | |
| while (index < line.length) { | |
| character = line.substr(index, 1); | |
| if (character === '{') { | |
| openBraces++; | |
| } | |
| if (character === '}') { | |
| openBraces--; | |
| } | |
| if (openBraces === 0) { | |
| findwhile = true; | |
| } else { | |
| findwhile = false; | |
| } | |
| if (openBraces === 0) { | |
| DEBUG && debug('outside of closure, looking for `while` statement: ' + line); // jshint ignore:line | |
| } | |
| if (findwhile && line.indexOf('while') !== -1) { | |
| DEBUG && debug('- exit as we found `while`: ' + line); // jshint ignore:line | |
| pushonly[lineNum] = true; | |
| return; | |
| } | |
| index++; | |
| if (index === line.length && lineNum < (lines.length-1)) { | |
| lineNum++; | |
| line = lines[lineNum]; | |
| DEBUG && debug(line); // jshint ignore:line | |
| index = 0; | |
| } | |
| } | |
| return; | |
| } | |
| } | |
| index++; | |
| if (index === line.length && lineNum < (lines.length-1)) { | |
| // move to the next line | |
| DEBUG && debug('- moving to next line'); // jshint ignore:line | |
| recompiled.push(line); | |
| lineNum++; | |
| line = lines[lineNum]; | |
| ignore[lineNum] = true; | |
| index = 0; | |
| } | |
| } | |
| } else { | |
| // else we're a regular line, and we shouldn't be touched | |
| DEBUG && debug('regular line ' + line); // jshint ignore:line | |
| recompiled.push(line); | |
| } | |
| }); | |
| DEBUG && debug('---- source ----'); // jshint ignore:line | |
| DEBUG && debug(code); // jshint ignore:line | |
| DEBUG && debug('---- rewrite ---'); // jshint ignore:line | |
| DEBUG && debug(recompiled.join('\n')); // jshint ignore:line | |
| DEBUG && debug(''); // jshint ignore:line | |
| return disableLoopProtection ? code : recompiled.join('\n'); | |
| }; | |
| /** | |
| * Injected code in to user's code to **try** to protect against infinite | |
| * loops cropping up in the code, and killing the browser. Returns true | |
| * when the loops has been running for more than 100ms. | |
| */ | |
| loopProtect.protect = function protect(state) { | |
| loopProtect.counters[state.line] = loopProtect.counters[state.line] || {}; | |
| var line = loopProtect.counters[state.line]; | |
| var now = (new Date()).getTime(); | |
| if (state.reset) { | |
| line.time = now; | |
| line.hit = 0; | |
| line.last = 0; | |
| } | |
| line.hit++; | |
| if ((now - line.time) > loopTimeout) {//} && line.hit !== line.last+1) { | |
| loopProtect.hit(state.line); | |
| // Returning true prevents the loop running again | |
| return true; | |
| } | |
| line.last++; | |
| return false; | |
| }; | |
| loopProtect.hit = function hit(line) { | |
| var msg = 'Exiting potential infinite loop at line ' + line + '. To disable loop protection: add "// noprotect" to your code'; | |
| if (root.proxyConsole) { | |
| root.proxyConsole.error(msg); | |
| } else { | |
| console.error(msg); | |
| } | |
| }; | |
| loopProtect.reset = function reset() { | |
| // reset the counters | |
| loopProtect.counters = {}; | |
| }; | |
| return loopProtect; | |
| }); |