UNPKG

@awayfl/avm2

Version:

Virtual machine for executing AS3 code

397 lines (348 loc) 9.61 kB
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; } }