@awayfl/avm2
Version:
Virtual machine for executing AS3 code
390 lines (389 loc) • 14.4 kB
JavaScript
import { __extends } from "tslib";
import { transformJStoASRegExpMatchArray } from './transformJStoASRegExpMatchArray';
import { ASObject } from './ASObject';
import { addPrototypeFunctionAlias } from './addPrototypeFunctionAlias';
import { Errors } from '../errors';
import { Settings } from '../Settings';
import { XRegExp } from './XRegLookup';
var WARN_REPORT_TABLE = {};
var IS_SUPPORT_LOOKBEHIND = (function () {
try {
new RegExp('(?<=)');
return true;
}
catch (_) {
return false;
}
})();
var ASRegExp = /** @class */ (function (_super) {
__extends(ASRegExp, _super);
function ASRegExp(pattern, flags) {
var _this = _super.call(this) || this;
_this._flags = '';
_this._useFallback = false;
_this._dotall = false;
_this._extended = false;
_this._captureNames = [];
var source;
if (pattern === undefined) {
pattern = source = '';
}
else if (_this.sec.AXRegExp.axIsType(pattern)) {
if (flags) {
_this.sec.throwError('TypeError', Errors.RegExpFlagsArgumentError);
}
flags = pattern._flags;
source = pattern.source;
pattern = pattern.value;
}
else {
pattern = String(pattern);
// Escape all forward slashes.
source = pattern.replace(/(^|^[/]|(?:\\\\)+)\//g, '$1\\/');
if (flags) {
var f = flags;
flags = '';
for (var i = 0; i < f.length; i++) {
var flag = f[i];
switch (flag) {
case 's':
// With the s flag set, . will match the newline character.
_this._dotall = true;
break;
case 'x':
// With the x flag set, spaces in the regular expression, will be ignored as part of
// the pattern.
_this._extended = true;
break;
case 'g':
case 'i':
case 'm':
// Only keep valid flags since an ECMAScript compatible RegExp implementation will
// throw on invalid ones. We have to avoid that in ActionScript.
flags += flag;
}
}
}
_this._flags = flags || '';
pattern = _this._parse(source);
}
try {
_this.value = new RegExp(pattern, flags);
}
catch (e) {
console.log('Unsupported RegExp pattern:' + pattern);
_this._useFallback = true;
}
_this._source = source;
return _this;
}
ASRegExp.classInitializer = function () {
var proto = this.dPrototype;
var asProto = ASRegExp.prototype;
addPrototypeFunctionAlias(proto, '$BgtoString', asProto.ecmaToString);
addPrototypeFunctionAlias(proto, '$Bgexec', asProto.exec);
addPrototypeFunctionAlias(proto, '$Bgtest', asProto.test);
};
// Parses and sanitizes a AS3 RegExp pattern to be used in JavaScript. Silently fails and
// returns an unmatchable pattern of the source turns out to be invalid.
ASRegExp.prototype._parse = function (pattern) {
if (pattern.includes('(?<=') || pattern.includes('(?<!') && !this._extended) {
if (!IS_SUPPORT_LOOKBEHIND) {
if (!Settings.EMULATE_LOOKBEHIND) {
throw new Error('[ASRegExp] Pattern include a lookbehind, but your browser not usupport it:' + pattern);
}
WARN_REPORT_TABLE['lookbehind'] || console.warn('[ASRegExp] Pattern include a lookbehind, we should use XRegExp polyfill for Safari.\n', pattern);
WARN_REPORT_TABLE['lookbehind'] = true;
// falling down to XRegExp
this._useFallback = true;
return pattern;
}
WARN_REPORT_TABLE['lookbehind'] || console.warn('[ASRegExp] Pattern include a lookbehind, we will use a native .\n', pattern);
WARN_REPORT_TABLE['lookbehind'] = true;
return pattern;
}
var result = '';
var captureNames = this._captureNames;
var parens = [];
var atoms = 0;
for (var i = 0; i < pattern.length; i++) {
var char = pattern[i];
switch (char) {
case '(':
result += char;
parens.push(atoms > 1 ? atoms - 1 : atoms);
atoms = 0;
if (pattern[i + 1] === '?') {
switch (pattern[i + 2]) {
case ':':
case '=':
case '!':
result += '?' + pattern[i + 2];
i += 2;
break;
default:
if (/\(\?P<([\w$]+)>/.exec(pattern.substr(i))) {
var name_1 = RegExp.$1;
if (name_1 !== 'length') {
captureNames.push(name_1);
}
if (captureNames.indexOf(name_1) > -1) {
// TODO: Handle the case were same name is used for multiple groups.
}
i += RegExp.lastMatch.length - 1;
}
else {
return ASRegExp.UNMATCHABLE_PATTERN;
}
}
}
else {
captureNames.push(null);
}
// 406 seems to be the maximum number of capturing groups allowed in a pattern.
// Examined by testing.
if (captureNames.length > 406) {
return ASRegExp.UNMATCHABLE_PATTERN;
}
break;
case ')':
if (!parens.length) {
return ASRegExp.UNMATCHABLE_PATTERN;
}
result += char;
atoms = parens.pop() + 1;
break;
case '|':
result += char;
break;
case '\\':
result += char;
if (/\\|c[A-Z]|x[0-9,a-z,A-Z]{2}|u[0-9,a-z,A-Z]{4}|./.exec(pattern.substr(i + 1))) {
result += RegExp.lastMatch;
i += RegExp.lastMatch.length;
}
if (atoms <= 1) {
atoms++;
}
break;
case '[':
if (/\[[^\]]*\]/.exec(pattern.substr(i))) {
result += RegExp.lastMatch;
i += RegExp.lastMatch.length - 1;
if (atoms <= 1) {
atoms++;
}
}
else {
return ASRegExp.UNMATCHABLE_PATTERN;
}
break;
case '{':
if (/\{[^{]*?(?:,[^{]*?)?\}/.exec(pattern.substr(i))) {
result += RegExp.lastMatch;
i += RegExp.lastMatch.length - 1;
}
else {
return ASRegExp.UNMATCHABLE_PATTERN;
}
break;
case '.':
if (this._dotall) {
result += '[\\s\\S]';
}
else {
result += char;
}
if (atoms <= 1) {
atoms++;
}
break;
case '?':
case '*':
case '+':
if (!atoms) {
return ASRegExp.UNMATCHABLE_PATTERN;
}
result += char;
if (pattern[i + 1] === '?') {
i++;
result += '?';
}
break;
case ' ':
{
if (this._extended) {
break;
}
result += char;
if (atoms <= 1) {
atoms++;
}
break;
}
default: {
result += char;
if (atoms <= 1) {
atoms++;
}
}
}
// 32767 seams to be the maximum allowed length for RegExps in SpiderMonkey.
// Examined by testing.
if (result.length > 0x7fff) {
return ASRegExp.UNMATCHABLE_PATTERN;
}
}
if (parens.length) {
return ASRegExp.UNMATCHABLE_PATTERN;
}
return result;
};
ASRegExp.prototype.ecmaToString = function () {
var out = '/' + this._source + '/';
if (this.value.global)
out += 'g';
if (this.value.ignoreCase)
out += 'i';
if (this.value.multiline)
out += 'm';
if (this._dotall)
out += 's';
if (this._extended)
out += 'x';
return out;
};
ASRegExp.prototype.axCall = function (_) {
// eslint-disable-next-line
return this.exec.apply(this, arguments);
};
ASRegExp.prototype.axApply = function (_, argArray) {
// eslint-disable-next-line
return this.exec.apply(this, argArray);
};
Object.defineProperty(ASRegExp.prototype, "source", {
get: function () {
return this._source;
},
enumerable: false,
configurable: true
});
Object.defineProperty(ASRegExp.prototype, "global", {
get: function () {
return this.value.global;
},
enumerable: false,
configurable: true
});
Object.defineProperty(ASRegExp.prototype, "ignoreCase", {
get: function () {
return this.value.ignoreCase;
},
enumerable: false,
configurable: true
});
Object.defineProperty(ASRegExp.prototype, "multiline", {
get: function () {
return this.value.multiline;
},
enumerable: false,
configurable: true
});
Object.defineProperty(ASRegExp.prototype, "lastIndex", {
get: function () {
return this.value.lastIndex;
},
set: function (value) {
this.value.lastIndex = value;
},
enumerable: false,
configurable: true
});
Object.defineProperty(ASRegExp.prototype, "dotall", {
get: function () {
return this._dotall;
},
enumerable: false,
configurable: true
});
Object.defineProperty(ASRegExp.prototype, "extended", {
get: function () {
return this._extended;
},
enumerable: false,
configurable: true
});
ASRegExp.prototype.internalStringSearch = function (string) {
if (!this._useFallback) {
return string.search(this.value);
}
return XRegExp.searchLb(string, this._source, this._flags);
};
ASRegExp.prototype.internalStringReplace = function (string, replace) {
if (!this._useFallback) {
return string.replace(this.value, replace);
}
return XRegExp.replaceLb(string, this._source, replace, this._flags);
};
// box string matche from string
ASRegExp.prototype.internalStringMatch = function (string) {
var g = this._flags.includes('g');
if (!this._useFallback) {
var res_1 = string.match(this.value);
// in Flash match should return a [] for globals
if (!res_1) {
return g ? [] : null;
}
return res_1;
}
var res = XRegExp.matchAllLb(string, this._source, this._flags);
if (res.length > 0) {
var match = [res[0]];
/**
* XRegExp not fully implement lookup behind matching, set index to 0
* @todo Maybe dangerous, implement it!
*/
match.index = 0;
match.input = string;
return match;
}
else if (g) {
return [];
}
return null;
};
ASRegExp.prototype.exec = function (str) {
if (str === void 0) { str = ''; }
var result;
if (this._useFallback) {
result = XRegExp.execLb(str, this._source, this._flags);
}
else {
result = this.value.exec(str);
}
if (!result) {
return null;
}
var axResult = transformJStoASRegExpMatchArray(this.sec, result);
var captureNames = this._captureNames;
if (captureNames) {
for (var i = 0; i < captureNames.length; i++) {
var name_2 = captureNames[i];
if (name_2 !== null) {
// In AS3, non-matched named capturing groups return an empty string.
var value = result[i + 1] || '';
result[name_2] = value;
axResult.axSetPublicProperty(name_2, value);
}
}
return axResult;
}
};
ASRegExp.prototype.test = function (str) {
if (str === void 0) { str = ''; }
return this.exec(str) !== null;
};
ASRegExp.UNMATCHABLE_PATTERN = '^(?!)$';
return ASRegExp;
}(ASObject));
export { ASRegExp };