@awayfl/avm2
Version:
Virtual machine for executing AS3 code
692 lines (616 loc) • 20.1 kB
text/typescript
import { AXApplicationDomain } from '../../run/AXApplicationDomain';
import { assert } from '@awayjs/graphics';
import { release, unexpected, warning } from '@awayfl/swf-loader';
import { Namespace } from './Namespace';
import { Multiname } from './Multiname';
import { MetadataInfo } from './MetadataInfo';
import { MethodInfo } from './MethodInfo';
import { MethodBodyInfo } from './MethodBodyInfo';
import { ClassInfo } from './ClassInfo';
import { ScriptInfo } from './ScriptInfo';
import { InstanceInfo } from './InstanceInfo';
import { CONSTANT } from './CONSTANT';
import { Errors } from '../../errors';
import { NamespaceType } from './NamespaceType';
import { internNamespace } from './internNamespace';
import { METHOD } from './METHOD';
import { ParameterInfo } from './ParameterInfo';
import { Traits } from './Traits';
import { TraitInfo } from './TraitInfo';
import { TRAIT } from './TRAIT';
import { SlotTraitInfo } from './SlotTraitInfo';
import { MethodTraitInfo } from './MethodTraitInfo';
import { ClassTraitInfo } from './ClassTraitInfo';
import { ATTR } from './ATTR';
import { ExceptionInfo } from './ExceptionInfo';
import { IndentingWriter } from '@awayfl/swf-loader';
import { AbcStream } from '../stream';
export class ABCFile {
public ints: Int32Array;
public uints: Uint32Array;
public doubles: Float64Array;
/**
* Environment this ABC is loaded into.
* In the shell, this is just a wrapper around an applicationDomain, but in the
* SWF player, it's a flash.display.LoaderInfo object.
*/
public env: {app: AXApplicationDomain; url: string};
public get applicationDomain() {
release || assert(this.env.app);
return this.env.app;
}
private _stream: AbcStream;
private _strings: string [];
private _namespaces: Namespace [];
private _namespaceSets: Namespace [][];
private _multinames: Multiname [];
private _deferredMultinames: number[][] = [];
private _metadata: MetadataInfo [];
private _methods: MethodInfo [];
private _methodBodies: MethodBodyInfo [];
public classes: ClassInfo [];
public scripts: ScriptInfo [];
public instances: InstanceInfo [];
constructor(
env: {app: AXApplicationDomain; url: string},
private _buffer: Uint8Array
) {
this.env = env;
this._stream = new AbcStream(_buffer);
this._checkMagic();
this._parseNumericConstants();
this._parseStringConstants();
this._parseNamespaces();
this._parseNamespaceSets();
this._parseMultinames();
this._parseMethodInfos();
this._parseMetaData();
this._parseInstanceAndClassInfos();
this._parseScriptInfos();
this._parseMethodBodyInfos();
}
private _parseNumericConstants() {
const s = this._stream;
// Parse Signed Integers
let n = s.readU30();
const ints = new Int32Array(n);
ints[0] = 0;
for (let i = 1; i < n; i++) {
ints[i] = s.readS32();
}
this.ints = ints;
// Parse Unsigned Integers
n = s.readU30();
const uints = new Uint32Array(n);
uints[0] = 0;
for (let i = 1; i < n; i++) {
uints[i] = s.readS32();
}
this.uints = uints;
// Parse Doubles
n = s.readU30();
const doubles = new Float64Array(n);
doubles[0] = NaN;
for (let i = 1; i < n; i++) {
doubles[i] = s.readDouble();
}
this.doubles = doubles;
}
private _parseStringConstants() {
const s = this._stream;
const n = s.readU30();
this._strings = new Array(n);
this._strings[0] = null;
// Record the offset of each string in |stringOffsets|. This array has one extra
// element so that we can compute the length of the last string.
for (let i = 1; i < n; i++) {
const l = s.readU30();
this._strings[i] = s.readUTFString(l);
}
}
private _parseNamespaces() {
const s = this._stream;
const n = s.readU30();
this._namespaces = new Array(n);
this._namespaces[0] = Namespace.PUBLIC;
for (let i = 1; i < n; i++) {
const kind = s.readU8();
const uriIndex = s.readU30();
let uri = this._strings[uriIndex];
let type: NamespaceType;
switch (kind) {
case CONSTANT.Namespace:
case CONSTANT.PackageNamespace:
type = NamespaceType.Public;
break;
case CONSTANT.PackageInternalNs:
type = NamespaceType.PackageInternal;
break;
case CONSTANT.ProtectedNamespace:
type = NamespaceType.Protected;
break;
case CONSTANT.ExplicitNamespace:
type = NamespaceType.Explicit;
break;
case CONSTANT.StaticProtectedNs:
type = NamespaceType.StaticProtected;
break;
case CONSTANT.PrivateNs:
type = NamespaceType.Private;
break;
default:
this.applicationDomain.sec.throwError('VerifierError',
Errors.CpoolEntryWrongTypeError, i);
}
if (uri && type !== NamespaceType.Private) {
// TODO: deal with API versions here. Those are suffixed to the uri. We used to
// just strip them out, but we also had an assert against them occurring at all,
// so it might be the case that we don't even need to do anything at all.
} else if (uri === null) {
// Only private namespaces gets the empty string instead of undefined. A comment
// in Tamarin source code indicates this might not be intentional, but oh well.
uri = '';
}
this._namespaces[i] = internNamespace(type, uri);
}
}
private _parseNamespaceSets() {
const s = this._stream;
const n = s.readU30();
this._namespaceSets = new Array(n);
this._namespaceSets[0] = null;
for (let i = 1; i < n; i++) {
const c = s.readU30(); // Count
const nss = this._namespaceSets[i] = new Array(c);
for (let j = 0; j < c; j++) {
nss[j] = this._namespaces[s.readU30()];
}
}
}
private _parseMultinames() {
const s = this._stream;
const n = s.readU30();
this._multinames = new Array(n);
this._multinames[0] = null;
for (let i = 1; i < n; i++) {
this._multinames[i] = this._parseMultiname(i);
}
const o = s.position;
while (this._deferredMultinames.length) {
const [i, o] = this._deferredMultinames.shift();
s.seek(o);
this._multinames[i] = this._parseMultiname(i);
}
s.seek(o);
}
private _parseMultiname(i: number): Multiname {
const stream = this._stream;
const o = stream.position;
let namespaceIsRuntime = false;
let namespaceIndex;
let useNamespaceSet = true;
let nameIndex = 0;
const kind = stream.readU8();
switch (kind) {
case CONSTANT.QName:
case CONSTANT.QNameA:
namespaceIndex = stream.readU30();
useNamespaceSet = false;
nameIndex = stream.readU30();
break;
case CONSTANT.RTQName: case CONSTANT.RTQNameA:
namespaceIsRuntime = true;
nameIndex = stream.readU30();
break;
case CONSTANT.RTQNameL: case CONSTANT.RTQNameLA:
namespaceIsRuntime = true;
break;
case CONSTANT.Multiname: case CONSTANT.MultinameA:
nameIndex = stream.readU30();
namespaceIndex = stream.readU30();
break;
case CONSTANT.MultinameL: case CONSTANT.MultinameLA:
namespaceIndex = stream.readU30();
if (!release && namespaceIndex === 0) {
// TODO: figure out what to do in this case. What would Tamarin do?
warning('Invalid multiname: namespace-set index is 0');
}
break;
/**
* This is undocumented, looking at Tamarin source for this one.
*/
case CONSTANT.TypeName: {
const nameIndex = stream.readU32();
const typeParameterCount = stream.readU32();
if (!release && typeParameterCount !== 1) {
// TODO: figure out what to do in this case. What would Tamarin do?
warning('Invalid multiname: bad type parameter count ' + typeParameterCount);
}
const typeParameter = this._multinames[stream.readU32()];
const factory = this._multinames[nameIndex];
if (typeParameter == null || factory == null) {
this._deferredMultinames.push([i, o]);
return;
}
return new Multiname(this, i, kind, factory.namespaces, factory.name, typeParameter);
}
default:
unexpected();
break;
}
// A name index of 0 means that it's a runtime name.
const name = nameIndex === 0 ? null : this._strings[nameIndex];
const namespaces = namespaceIsRuntime
? null
: useNamespaceSet
? this._namespaceSets[namespaceIndex]
: [this._namespaces[namespaceIndex]];
return new Multiname(this, i, kind, namespaces, name);
}
private _checkMagic() {
const magic = this._stream.readWord();
const flashPlayerBrannan = 46 << 16 | 15;
if (magic < flashPlayerBrannan) {
this.env.app.sec.throwError('VerifierError', Errors.InvalidMagicError, magic >> 16,
magic & 0xffff);
}
}
/**
* String duplicates exist in practice but are extremely rare.
*/
private _checkForDuplicateStrings(): boolean {
const a = [];
for (let i = 0; i < this._strings.length; i++) {
a.push(this._strings[i]);
}
a.sort();
for (let i = 0; i < a.length - 1; i++) {
if (a[i] === a[i + 1]) {
return true;
}
}
return false;
}
/**
* Returns the string at the specified index in the string table.
*/
public getString(i: number): string {
release || assert(i >= 0 && i < this._strings.length);
return this._strings[i];
}
/**
* Returns the multiname at the specified index in the multiname table.
*/
public getMultiname(i: number): Multiname {
if (i < 0 || i >= this._multinames.length) {
this.applicationDomain.sec.throwError('VerifierError',
Errors.CpoolIndexRangeError, i,
this._multinames.length);
}
return (i !== 0) ? this._multinames[i] || (this._multinames[i] = this._parseMultiname(i)) : null;
}
/**
* Returns the namespace at the specified index in the namespace table.
*/
public getNamespace(i: number): Namespace {
if (i < 0 || i >= this._namespaces.length) {
this.applicationDomain.sec.throwError('VerifierError', Errors.CpoolIndexRangeError, i,
this._namespaces.length);
}
return this._namespaces[i];
}
/**
* Returns the namespace set at the specified index in the namespace set table.
*/
public getNamespaceSet(i: number): Namespace [] {
if (i < 0 || i >= this._namespaceSets.length) {
this.applicationDomain.sec.throwError('VerifierError', Errors.CpoolIndexRangeError, i,
this._namespaceSets.length);
}
return this._namespaceSets[i];
}
private _parseMethodInfos() {
const s = this._stream;
const n = s.readU30();
this._methods = new Array(n);
for (let i = 0; i < n; ++i) {
this._methods[i] = this._parseMethodInfo(i);
}
}
private _parseMethodInfo(j: number) {
const s = this._stream;
const parameterCount = s.readU30();
const returnType = this._multinames[s.readU30()];
const parameters = new Array<ParameterInfo>(parameterCount);
for (let i = 0; i < parameterCount; i++) {
parameters[i] = new ParameterInfo(this, this._multinames[s.readU30()]);
}
const name = this._strings[s.readU30()] || 'anonymous';
const flags = s.readU8();
let optionalCount = 0;
if (flags & METHOD.HasOptional) {
optionalCount = s.readU30();
release || assert(parameterCount >= optionalCount);
for (let i = parameterCount - optionalCount; i < parameterCount; i++) {
parameters[i].optionalValueIndex = s.readU30();
parameters[i].optionalValueKind = s.readU8();
}
}
if (flags & METHOD.HasParamNames) {
for (let i = 0; i < parameterCount; i++) {
// NOTE: We can't get the parameter name as described in the spec because some SWFs have
// invalid parameter names. Tamarin ignores parameter names and so do we.
parameters[i].name = this._strings[s.readU30()];
}
}
return new MethodInfo(this, j, name, returnType, parameters, optionalCount, flags);
}
/**
* Returns the method info at the specified index in the method info table.
*/
public getMethodInfo(i: number) {
release || assert(i >= 0 && i < this._methods.length);
return this._methods[i];
}
public getMethodBodyInfo(i: number) {
return this._methodBodies[i];
}
private _parseMetaData() {
const s = this._stream;
const n = s.readU30();
this._metadata = new Array(n);
for (let i = 0; i < n; i++) {
const name = this._strings[s.readU30()]; // Name
const itemCount = s.readU30(); // Item Count
const keys = new Array(itemCount);
for (let j = 0; j < itemCount; j++) {
keys[j] = this._strings[s.readU30()];
}
const values = new Array(itemCount);
for (let j = 0; j < itemCount; j++) {
values[j] = this._strings[s.readU30()];
}
this._metadata[i] = new MetadataInfo(this, name, keys, values);
}
}
public getMetadataInfo(i: number): MetadataInfo {
release || assert(i >= 0 && i < this._metadata.length);
return this._metadata[i];
}
private _parseInstanceAndClassInfos() {
const s = this._stream;
const n = s.readU30();
const instances = this.instances = new Array(n);
for (let i = 0; i < n; i++) {
instances[i] = this._parseInstanceInfo();
}
this._parseClassInfos(n);
for (let i = 0; i < n; i++) {
instances[i].classInfo = this.classes[i];
}
}
private _parseInstanceInfo(): InstanceInfo {
const s = this._stream;
const multiname = this._multinames[s.readU30()];
const superName = this._multinames[s.readU30()];
const flags = s.readU8();
const protectedNs = (flags & CONSTANT.ClassProtectedNs) ? this._namespaces[s.readU30()] : Namespace.PUBLIC;
const interfaceCount = s.readU30();
const interfaces: Multiname[] = [];
for (let i = 0; i < interfaceCount; i++) {
interfaces[i] = this._multinames[s.readU30()];
}
const methodInfo = this._methods[s.readU30()];
const traits = this._parseTraits();
const instanceInfo = new InstanceInfo(this, multiname, superName, flags, protectedNs,
interfaces, methodInfo, traits);
traits.attachHolder(instanceInfo);
return instanceInfo;
}
private _parseTraits(global: boolean = false) {
const s = this._stream;
const n = s.readU30();
const traits = new Array(n);
for (let i = 0; i < n; i++) {
traits[i] = this._parseTrait();
}
return new Traits(traits, global);
}
private _parseTrait() {
const s = this._stream;
const multiname = this._multinames[s.readU30()];
const tag = s.readU8();
const kind = tag & 0x0F;
const attributes = (tag >> 4) & 0x0F;
let trait: TraitInfo;
switch (kind) {
case TRAIT.Slot:
case TRAIT.Const: {
const slot = s.readU30();
const typeName = this._multinames[s.readU30()];
const valueIndex = s.readU30();
let valueKind = -1;
if (valueIndex !== 0) {
valueKind = s.readU8();
}
trait = new SlotTraitInfo(this, kind, multiname, slot, typeName, valueKind, valueIndex);
break;
}
case TRAIT.Method:
case TRAIT.Getter:
case TRAIT.Setter: {
s.readU30(); // Tamarin optimization.
const methodInfo = this._methods[s.readU30()];
trait = methodInfo.trait = new MethodTraitInfo(this, kind, multiname, methodInfo);
break;
}
case TRAIT.Class: {
const slot = s.readU30();
const classInfo = this.classes[s.readU30()];
trait = classInfo.trait = new ClassTraitInfo(this, kind, multiname, slot, classInfo);
break;
}
default:
this.applicationDomain.sec.throwError('VerifierError',
Errors.UnsupportedTraitsKindError, kind);
}
if (attributes & ATTR.Metadata) {
const n = s.readU30();
const metadata: MetadataInfo[] = new Array(n);
for (let i = 0; i < n; i++) {
metadata[i] = this._metadata[s.readU30()];
}
trait.metadata = metadata;
}
return trait;
}
private _parseClassInfos(n: number) {
const classes = this.classes = new Array(n);
for (let i = 0; i < n; i++) {
classes[i] = this._parseClassInfo(i);
}
}
private _parseClassInfo(i: number) {
const methodInfo = this._methods[this._stream.readU30()];
const traits = this._parseTraits(true);
const classInfo = new ClassInfo(this, this.instances[i], methodInfo, traits);
traits.attachHolder(classInfo);
return classInfo;
}
private _parseScriptInfos() {
const n = this._stream.readU30();
const scripts = this.scripts = new Array(n);
for (let i = 0; i < n; i++) {
scripts[i] = this._parseScriptInfo();
}
}
private _parseScriptInfo() {
const methodInfo = this._methods[this._stream.readU30()];
const traits = this._parseTraits(true);
const scriptInfo = new ScriptInfo(this, methodInfo, traits);
traits.attachHolder(scriptInfo);
return scriptInfo;
}
private _parseMethodBodyInfos() {
const s = this._stream;
const methodBodies = this._methodBodies = new Array(this._methods.length);
const n = s.readU30();
for (let i = 0; i < n; i++) {
const methodInfo = s.readU30();
const maxStack = s.readU30();
const localCount = s.readU30();
const initScopeDepth = s.readU30();
const maxScopeDepth = s.readU30();
const code = s.viewU8s(s.readU30());
const e = s.readU30();
const exceptions = new Array(e);
for (let j = 0; j < e; ++j) {
exceptions[j] = this._parseException();
}
const traits = this._parseTraits();
methodBodies[methodInfo]
= new MethodBodyInfo(maxStack, localCount, initScopeDepth, maxScopeDepth, code, exceptions, traits);
traits.attachHolder(methodBodies[methodInfo]);
}
}
private _parseException() {
const s = this._stream;
const start = s.readU30();
const end = s.readU30();
const target = s.readU30();
const typeIndex = s.readU30();
const nameIndex = s.readU30();
return new ExceptionInfo(this, start, end, target, this._multinames[nameIndex], this._multinames[typeIndex]);
}
public getConstant(kind: CONSTANT, i: number): any {
switch (kind) {
case CONSTANT.Int:
return this.ints[i];
case CONSTANT.UInt:
return this.uints[i];
case CONSTANT.Double:
return this.doubles[i];
case CONSTANT.Utf8:
return this._strings[i];
case CONSTANT.True:
return true;
case CONSTANT.False:
return false;
case CONSTANT.Null:
return null;
case CONSTANT.Undefined:
return undefined;
case CONSTANT.Namespace:
case CONSTANT.PackageInternalNs:
return this._namespaces[i];
case CONSTANT.QName:
case CONSTANT.MultinameA:
case CONSTANT.RTQName:
case CONSTANT.RTQNameA:
case CONSTANT.RTQNameL:
case CONSTANT.RTQNameLA:
case CONSTANT.NameL:
case CONSTANT.NameLA:
return this._multinames[i];
case CONSTANT.Float:
warning('TODO: CONSTANT.Float may be deprecated?');
break;
default:
release || assert(false, 'Not Implemented Kind ' + kind);
}
}
trace(writer: IndentingWriter) {
writer.writeLn('Multinames: ' + this._multinames.length);
writer.indent();
for (let i = 0; i < this._multinames.length; i++) {
writer.writeLn(i + ' ' + this._multinames[i]);
}
writer.outdent();
writer.writeLn('Namespace Sets: ' + this._namespaceSets.length);
writer.indent();
for (let i = 0; i < this._namespaceSets.length; i++) {
writer.writeLn(i + ' ' + this._multinames[i]);
}
writer.outdent();
writer.writeLn('Namespaces: ' + this._namespaces.length);
writer.indent();
for (let i = 0; i < this._namespaces.length; i++) {
writer.writeLn(i + ' ' + this._namespaces[i]);
}
writer.outdent();
writer.writeLn('Strings: ' + this._strings.length);
writer.indent();
for (let i = 0; i < this._strings.length; i++) {
writer.writeLn(i + ' ' + this._strings[i]);
}
writer.outdent();
writer.writeLn('MethodInfos: ' + this._methods.length);
writer.indent();
for (let i = 0; i < this._methods.length; i++) {
writer.writeLn(i + ' ' + this.getMethodInfo(i));
if (this._methodBodies[i]) {
this._methodBodies[i].trace(writer);
}
}
writer.outdent();
writer.writeLn('InstanceInfos: ' + this.instances.length);
writer.indent();
for (let i = 0; i < this.instances.length; i++) {
writer.writeLn(i + ' ' + this.instances[i]);
this.instances[i].trace(writer);
}
writer.outdent();
writer.writeLn('ClassInfos: ' + this.classes.length);
writer.indent();
for (let i = 0; i < this.classes.length; i++) {
this.classes[i].trace(writer);
}
writer.outdent();
writer.writeLn('ScriptInfos: ' + this.scripts.length);
writer.indent();
for (let i = 0; i < this.scripts.length; i++) {
this.scripts[i].trace(writer);
}
writer.outdent();
}
}