@awayfl/avm2
Version:
Virtual machine for executing AS3 code
397 lines (348 loc) • 9.61 kB
text/typescript
import { transformJStoASRegExpMatchArray } from './transformJStoASRegExpMatchArray';
import { ASObject } from './ASObject';
import { addPrototypeFunctionAlias } from './addPrototypeFunctionAlias';
import { Errors } from '../errors';
import { ASArray } from './ASArray';
import { Settings } from '../Settings';
import { XRegExp } from './XRegLookup';
import { ExecArray } from 'xregexp/types';
const WARN_REPORT_TABLE: StringMap<boolean> = {};
const IS_SUPPORT_LOOKBEHIND = (() => {
try {
new RegExp('(?<=)');
return true;
} catch (_) {
return false;
}
})();
export class ASRegExp extends ASObject {
private static UNMATCHABLE_PATTERN = '^(?!)$';
static classInitializer() {
const proto: any = this.dPrototype;
const asProto: any = ASRegExp.prototype;
addPrototypeFunctionAlias(proto, '$BgtoString', asProto.ecmaToString);
addPrototypeFunctionAlias(proto, '$Bgexec', asProto.exec);
addPrototypeFunctionAlias(proto, '$Bgtest', asProto.test);
}
public value: RegExp;
private _flags: string = '';
private _useFallback: boolean = false;
private _dotall: boolean;
private _extended: boolean;
private _source: string;
private _captureNames: string [];
constructor(pattern: any, flags?: string) {
super();
this._dotall = false;
this._extended = false;
this._captureNames = [];
let source: string;
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) {
const f = flags;
flags = '';
for (let i = 0; i < f.length; i++) {
const 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;
}
// 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.
private _parse(pattern: string): string {
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;
}
let result = '';
const captureNames = this._captureNames;
const parens = [];
let atoms = 0;
for (let i = 0; i < pattern.length; i++) {
const 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))) {
const name = RegExp.$1;
if (name !== 'length') {
captureNames.push(name);
}
if (captureNames.indexOf(name) > -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;
}
ecmaToString(): string {
let 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;
}
axCall(_: any): any {
// eslint-disable-next-line
return this.exec.apply(this, arguments);
}
axApply(_: any, argArray?: any[]): any {
// eslint-disable-next-line
return this.exec.apply(this, argArray);
}
get source(): string {
return this._source;
}
get global(): boolean {
return this.value.global;
}
get ignoreCase(): boolean {
return this.value.ignoreCase;
}
get multiline(): boolean {
return this.value.multiline;
}
get lastIndex(): number {
return this.value.lastIndex;
}
set lastIndex(value: number) {
this.value.lastIndex = value;
}
get dotall(): boolean {
return this._dotall;
}
get extended(): boolean {
return this._extended;
}
internalStringSearch (string: string): number {
if (!this._useFallback) {
return string.search(this.value);
}
return XRegExp.searchLb(string, this._source, this._flags);
}
internalStringReplace (string: string, replace: string | any): string {
if (!this._useFallback) {
return string.replace(this.value, replace);
}
return XRegExp.replaceLb(string, this._source, replace, this._flags);
}
// box string matche from string
internalStringMatch (string: string): any {
const g = this._flags.includes('g');
if (!this._useFallback) {
const res = string.match(this.value);
// in Flash match should return a [] for globals
if (!res) {
return g ? [] : null;
}
return res;
}
const res = XRegExp.matchAllLb(string, this._source, this._flags);
if (res.length > 0) {
const match = <any>[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;
}
exec(str: string = ''): ASArray {
let result: RegExpExecArray | ExecArray;
if (this._useFallback) {
result = XRegExp.execLb(str, this._source, this._flags);
} else {
result = this.value.exec(str);
}
if (!result) {
return null;
}
const axResult = transformJStoASRegExpMatchArray(this.sec, result);
const captureNames = this._captureNames;
if (captureNames) {
for (let i = 0; i < captureNames.length; i++) {
const name = captureNames[i];
if (name !== null) {
// In AS3, non-matched named capturing groups return an empty string.
const value = result[i + 1] || '';
result[name] = value;
axResult.axSetPublicProperty(name, value);
}
}
return axResult;
}
}
test(str: string = ''): boolean {
return this.exec(str) !== null;
}
}