@awayfl/avm2
Version:
Virtual machine for executing AS3 code
709 lines (665 loc) • 20.9 kB
text/typescript
/*
* Copyright 2014 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This file implements the AMF0 and AMF3 serialization protocols secified in:
* http://wwwimages.adobe.com/www.adobe.com/content/dam/Adobe/en/devnet/amf/pdf/amf-file-format-spec.pdf
*/
import { ASObject } from './nat/ASObject';
import { ASArray } from './nat/ASArray';
import { ByteArray } from './natives/byteArray';
import { assert } from '@awayjs/graphics';
import { release } from '@awayfl/swf-loader';
import { ByteArray as AwayByteArray } from '@awayjs/core';
import { StringUtilities, isNumeric } from '@awayfl/swf-loader';
import { ITraits } from './run/ITraits';
import { AXClass } from './run/AXClass';
import { AXBasePrototype } from './run/initializeAXBasePrototype';
import { forEachPublicProperty } from './run/forEachPublicProperty';
import { Float64Vector } from './natives/float64Vector';
import { Uint32Vector } from './natives/uint32Vector';
import { GenericVector } from './natives/GenericVector';
import { Int32Vector } from './natives/int32Vector';
class AMF3ReferenceTables {
strings: any [] = [];
objects: any [] = [];
traits: ITraits [] = [];
/**
* Trait names are kept in sync with |traits| and are used to optimize fetching public trait names.
*/
traitNames: string [] [] = [];
dynamic: boolean [] = [];
}
export class ClassAliases {
private _classMap: WeakMap<AXClass, string> = new WeakMap<AXClass, string>();
private _nameMap: any = Object.create(null);
getAliasByClass(axClass: AXClass): string {
return this._classMap.get(axClass);
}
getClassByAlias(alias: string): AXClass {
return this._nameMap[alias];
}
registerClassAlias(alias: string, axClass: AXClass) {
this._classMap.set(axClass, alias);
release || assert(!this._nameMap[alias] || (this._nameMap[alias] === axClass));
this._nameMap[alias] = axClass;
}
}
export const enum AMF0Marker {
NUMBER = 0x00,
BOOLEAN = 0x01,
STRING = 0x02,
OBJECT = 0x03,
NULL = 0x05,
UNDEFINED = 0x06,
REFERENCE = 0x07,
ECMA_ARRAY = 0x08,
OBJECT_END = 0x09,
STRICT_ARRAY = 0x0A,
DATE = 0x0B,
LONG_STRING = 0x0C,
XML = 0x0F,
TYPED_OBJECT = 0x10,
AVMPLUS = 0x11
}
function writeString(ba: ByteArray, s: string) {
if (s.length > 0xFFFF) {
throw 'AMF short string exceeded';
}
if (!s.length) {
ba.writeByte(0x00);
ba.writeByte(0x00);
return;
}
const bytes = StringUtilities.utf8decode(s);
ba.writeByte((bytes.length >> 8) & 255);
ba.writeByte(bytes.length & 255);
for (let i = 0; i < bytes.length; i++) {
ba.writeByte(bytes[i]);
}
}
function readString(ba: ByteArray): string {
const byteLength = (ba.readByte() << 8) | ba.readByte();
if (!byteLength) {
return '';
}
const buffer = new Uint8Array(byteLength);
for (let i = 0; i < byteLength; i++) {
buffer[i] = ba.readByte();
}
return StringUtilities.utf8encode(buffer);
}
function writeDouble(ba: ByteArray, value: number) {
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setFloat64(0, value, false);
for (let i = 0; i < buffer.byteLength; i++) {
ba.writeByte(view.getUint8(i));
}
}
function readDouble(ba: ByteArray): number {
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
for (let i = 0; i < buffer.byteLength; i++) {
view.setUint8(i, ba.readByte());
}
return view.getFloat64(0, false);
}
export class AMF0 {
public static write(ba: ByteArray, value: any) {
switch (typeof value) {
case 'boolean':
ba.writeByte(AMF0Marker.BOOLEAN);
ba.writeByte(value ? 0x01 : 0x00);
break;
case 'number':
ba.writeByte(AMF0Marker.NUMBER);
writeDouble(ba, value);
break;
case 'undefined':
ba.writeByte(AMF0Marker.UNDEFINED);
break;
case 'string':
ba.writeByte(AMF0Marker.STRING);
writeString(ba, value);
break;
case 'object': {
const object = (<ASObject>value);
release || assert(object === null || AXBasePrototype.isPrototypeOf(object));
if (object === null) {
ba.writeByte(AMF0Marker.NULL);
} else if (ba.sec.AXArray.axIsType(object)) {
const array = (<ASArray>object).value;
ba.writeByte(AMF0Marker.ECMA_ARRAY);
ba.writeByte((array.length >>> 24) & 255);
ba.writeByte((array.length >> 16) & 255);
ba.writeByte((array.length >> 8) & 255);
ba.writeByte(array.length & 255);
// REDUX: What about sparse arrays?
forEachPublicProperty(object, function (key: string, value: any) {
writeString(ba, key);
this.write(ba, value);
}, this);
ba.writeByte(0x00);
ba.writeByte(0x00);
ba.writeByte(AMF0Marker.OBJECT_END);
} else {
ba.writeByte(AMF0Marker.OBJECT);
forEachPublicProperty(object, function (key: string, value: any) {
writeString(ba, key);
this.write(ba, value);
}, this);
ba.writeByte(0x00);
ba.writeByte(0x00);
ba.writeByte(AMF0Marker.OBJECT_END);
}
return;
}
}
}
public static read(ba: ByteArray): any {
const marker = ba.readByte();
switch (marker) {
case AMF0Marker.NUMBER:
return readDouble(ba);
case AMF0Marker.BOOLEAN:
return !!ba.readByte();
case AMF0Marker.STRING:
return readString(ba);
case AMF0Marker.OBJECT: {
const object = ba.sec.createObject();
let key;
while ((key = readString(ba)).length) {
object.axSetPublicProperty(key, this.read(ba));
}
if (ba.readByte() !== AMF0Marker.OBJECT_END) {
throw 'AMF0 End marker is not found';
}
return object;
}
case AMF0Marker.NULL:
return null;
case AMF0Marker.UNDEFINED:
return undefined;
case AMF0Marker.ECMA_ARRAY: {
const array = ba.sec.createArray([]);
array.length = (ba.readByte() << 24) | (ba.readByte() << 16) |
(ba.readByte() << 8) | ba.readByte();
let key;
while ((key = readString(ba)).length) {
array.axSetPublicProperty(key, this.read(ba));
}
if (ba.readByte() !== AMF0Marker.OBJECT_END) {
throw 'AMF0 End marker is not found';
}
return array;
}
case AMF0Marker.STRICT_ARRAY: {
const array = ba.sec.createArray([]);
const length = array.length = (ba.readByte() << 24) | (ba.readByte() << 16) |
(ba.readByte() << 8) | ba.readByte();
for (let i = 0; i < length; i++) {
array.axSetPublicProperty(i, this.read(ba));
}
return array;
}
case AMF0Marker.AVMPLUS:
return readAMF3Value(ba, new AMF3ReferenceTables());
default:
throw 'AMF0 Unknown marker ' + marker;
}
}
}
export const enum AMF3Marker {
UNDEFINED = 0x00,
NULL = 0x01,
FALSE = 0x02,
TRUE = 0x03,
INTEGER = 0x04,
DOUBLE = 0x05,
STRING = 0x06,
XML_DOC = 0x07,
DATE = 0x08,
ARRAY = 0x09,
OBJECT = 0x0A,
XML = 0x0B,
BYTEARRAY = 0x0C,
VECTOR_INT = 0x0D,
VECTOR_UINT = 0x0E,
VECTOR_DOUBLE = 0x0F,
VECTOR_OBJECT = 0x10,
DICTIONARY = 0x11
}
function readU29(ba: ByteArray): number {
const b1 = ba.readByte();
if ((b1 & 0x80) === 0) {
return (b1 & 0x7F);
}
const b2 = ba.readByte();
if ((b2 & 0x80) === 0) {
return ((b1 & 0x7F) << 7) | (b2 & 0x7F);
}
const b3 = ba.readByte();
if ((b3 & 0x80) === 0) {
return ((b1 & 0x7F) << 14) | ((b2 & 0x7F) << 7) | (b3 & 0x7F);
}
const b4 = ba.readByte();
let val = ((b1 & 0x7f) << 22) | ((b2 & 0x7f) << 15) | ((b3 & 0x7f) << 8) | (b4 & 0xFF);
// handle negative
// https://github.com/Ventero/amf-cpp/blob/master/src/types/amfinteger.cpp#L92
// https://github.com/yzh44yzh/scala-amf/blob/master/scala-amf-lib/src/com/yzh44yzh/scalaAmf/AmfInt.scala#L35
if ((val & 0x10000000) !== 0) {
val |= 0xe0000000;
}
return val;
}
function writeU29(ba: ByteArray, value: number) {
// C++ version
// https://github.com/Ventero/amf-cpp/blob/master/src/types/amfinteger.cpp#L13
if (value < -0x10000000 || value >= 0x10000000) {
throw 'AMF3 U29 range';
}
if ((value & 0xFFFFFF80) === 0) {
ba.writeByte(value & 0x7F);
} else if ((value & 0xFFFFC000) === 0) {
ba.writeByte(0x80 | ((value >> 7) & 0x7F));
ba.writeByte(value & 0x7F);
} else if ((value & 0xFFE00000) === 0) {
ba.writeByte(0x80 | ((value >> 14) & 0x7F));
ba.writeByte(0x80 | ((value >> 7) & 0x7F));
ba.writeByte(value & 0x7F);
} else /*if ((value & 0xC0000000) === 0)*/{ // handle negative
ba.writeByte(0x80 | ((value >> 22) & 0x7F));
ba.writeByte(0x80 | ((value >> 15) & 0x7F));
ba.writeByte(0x80 | ((value >> 8) & 0x7F));
ba.writeByte(value & 0xFF);
}
}
function readUTF8(ba: ByteArray, references: AMF3ReferenceTables) {
const u29s = readU29(ba);
if (u29s === 0x01) {
return '';
}
const strings = references.strings;
if ((u29s & 1) === 0) {
return strings[u29s >> 1];
}
const byteLength = u29s >> 1;
const buffer = new Uint8Array(byteLength);
for (let i = 0; i < byteLength; i++) {
buffer[i] = ba.readByte();
}
const value = StringUtilities.utf8encode(buffer);
strings.push(value);
return value;
}
function writeUTF8(ba: ByteArray, s: string, references: AMF3ReferenceTables) {
if (s === '') {
ba.writeByte(0x01); // empty string
return;
}
const strings = references.strings;
const index = strings.indexOf(s);
if (index >= 0) {
writeU29(ba, index << 1);
return;
}
strings.push(s);
const bytes = StringUtilities.utf8decode(s);
writeU29(ba, 1 | (bytes.length << 1));
// we can write faster
if (ba instanceof AwayByteArray) {
// view can look on biggest array, we should check this
ba.writeArrayBuffer(
bytes.byteLength !== bytes.buffer.byteLength
? new Uint8Array(bytes).buffer
: bytes.buffer);
return;
}
for (let i = 0; i < bytes.length; i++) {
ba.writeByte(bytes[i]);
}
}
function readAMF3Value(ba: ByteArray, references: AMF3ReferenceTables) {
const marker = ba.readByte();
switch (marker) {
case AMF3Marker.NULL:
return null;
case AMF3Marker.UNDEFINED:
return undefined;
case AMF3Marker.FALSE:
return false;
case AMF3Marker.TRUE:
return true;
case AMF3Marker.INTEGER:
return readU29(ba);
case AMF3Marker.DOUBLE:
return readDouble(ba);
case AMF3Marker.STRING:
return readUTF8(ba, references);
case AMF3Marker.DATE: {
const u29o = readU29(ba);
release || assert((u29o & 1) === 1);
return ba.sec.AXDate.axConstruct([readDouble(ba)]);
}
case AMF3Marker.XML:
return ba.sec.AXXML.axConstruct([readUTF8(ba, references)]);
case AMF3Marker.OBJECT: {
const u29o = readU29(ba);
if ((u29o & 1) === 0) {
return references.objects[u29o >> 1];
}
let axClass: AXClass;
let traits: ITraits;
let isDynamic = true;
let traitNames;
if ((u29o & 2) === 0) {
traits = references.traits[u29o >> 2];
traitNames = references.traitNames[u29o >> 2];
isDynamic = references.dynamic[u29o >> 2];
} else {
const alias = readUTF8(ba, references);
if (alias) {
traits = axClass = ba.sec.classAliases.getClassByAlias(alias);
}
isDynamic = (u29o & 8) !== 0;
traitNames = [];
for (let i = 0, j = u29o >> 4; i < j; i++) {
traitNames.push(readUTF8(ba, references));
}
references.traits.push(traits);
references.traitNames.push(traitNames);
references.dynamic.push(isDynamic);
}
const object = axClass ? axClass.axConstruct([]) : ba.sec.createObject();
references.objects.push(object);
// Read trait properties.
for (let i = 0; i < traitNames.length; i++) {
const value = readAMF3Value(ba, references);
object.axSetPublicProperty(traitNames[i], value);
}
// Read dynamic properties.
if (isDynamic) {
let key;
while ((key = readUTF8(ba, references)) !== '') {
const value = readAMF3Value(ba, references);
object.axSetPublicProperty(key, value);
}
}
return object;
}
case AMF3Marker.ARRAY: {
const u29o = readU29(ba);
if ((u29o & 1) === 0) {
return references.objects[u29o >> 1];
}
const array = ba.sec.createArray([]);
references.objects.push(array);
const densePortionLength = u29o >> 1;
let key;
while ((key = readUTF8(ba, references)).length) {
const value = readAMF3Value(ba, references);
array.axSetPublicProperty(key, value);
}
for (let i = 0; i < densePortionLength; i++) {
const value = readAMF3Value(ba, references);
array.axSetPublicProperty(i, value);
}
return array;
}
case AMF3Marker.VECTOR_INT: {
const u29o = readU29(ba);
if ((u29o & 1) === 0) {
return references.objects[u29o >> 1];
}
const length = u29o >> 1;
const fixed = ba.readUnsignedInt();
const vector: Int32Vector = ba.sec.Int32Vector.axClass.axConstruct([length, fixed]);
references.objects.push(vector);
for (let i = 0; i < length; i++) {
vector.axSetPublicProperty(i, readU29(ba));
}
return vector;
}
case AMF3Marker.VECTOR_UINT: {
const u29o = readU29(ba);
if ((u29o & 1) === 0) {
return references.objects[u29o >> 1];
}
const length = u29o >> 1;
const fixed = ba.readUnsignedInt();
const vector: Uint32Vector = ba.sec.Uint32Vector.axClass.axConstruct([length, fixed]);
references.objects.push(vector);
for (let i = 0; i < length; i++) {
vector.axSetPublicProperty(i, readU29(ba));
}
return vector;
}
case AMF3Marker.VECTOR_DOUBLE: {
const u29o = readU29(ba);
if ((u29o & 1) === 0) {
return references.objects[u29o >> 1];
}
const length = u29o >> 1;
const fixed = ba.readUnsignedInt();
const vector: Float64Vector = ba.sec.Float64Vector.axClass.axConstruct([length, fixed]);
references.objects.push(vector);
for (let i = 0; i < length; i++) {
vector.axSetPublicProperty(i, readDouble(ba));
}
return vector;
}
case AMF3Marker.VECTOR_OBJECT: {
const u29o = readU29(ba);
if ((u29o & 1) === 0) {
return references.objects[u29o >> 1];
}
const length = u29o >> 1;
const fixed = ba.readUnsignedInt();
const type = ba.sec.classAliases.getClassByAlias(readUTF8(ba, references));
const vector: GenericVector = <any> ba.sec.getVectorClass(type).axConstruct([length, fixed]);
references.objects.push(vector);
for (let i = 0; i < length; i++) {
vector.axSetPublicProperty(i, readAMF3Value(ba, references));
}
return vector;
}
default:
throw 'AMF3 Unknown marker ' + marker;
}
}
/**
* Tries to write a reference to a previously written object.
*/
function tryWriteAndStartTrackingReference(ba: ByteArray, object: ASObject, references: AMF3ReferenceTables) {
const objects = references.objects;
const index = objects.indexOf(object);
if (index < 0) {
objects.push(object);
return false;
}
writeU29(ba, index << 1);
return true;
}
const MAX_INT = 268435456 - 1; // 2^28 - 1
const MIN_INT = -268435456; // -2^28
function writeAMF3Value(ba: ByteArray, value: any, references: AMF3ReferenceTables) {
switch (typeof value) {
case 'boolean':
ba.writeByte(value ? AMF3Marker.TRUE : AMF3Marker.FALSE);
break;
case 'number': {
let useInteger = value === (value | 0);
if (useInteger) {
if (value > MAX_INT || value < MIN_INT) {
useInteger = false;
}
}
if (useInteger) {
ba.writeByte(AMF3Marker.INTEGER);
writeU29(ba, value);
} else {
ba.writeByte(AMF3Marker.DOUBLE);
writeDouble(ba, value);
}
break;
}
case 'undefined':
ba.writeByte(AMF3Marker.UNDEFINED);
break;
case 'string':
ba.writeByte(AMF3Marker.STRING);
writeUTF8(ba, value, references);
break;
case 'object':
if (value === null) {
ba.writeByte(AMF3Marker.NULL);
} else if (ba.sec.AXArray.axIsType(value)) {
const array = (<ASArray>value);
ba.writeByte(AMF3Marker.ARRAY);
if (tryWriteAndStartTrackingReference(ba, array, references)) {
break;
}
let densePortionLength = 0;
while (array.axHasPublicProperty(densePortionLength)) {
++densePortionLength;
}
writeU29(ba, (densePortionLength << 1) | 1);
forEachPublicProperty(array, function (i: any, value: any) {
if (isNumeric(i) && i >= 0 && i < densePortionLength) {
return;
}
writeUTF8(ba, i, references);
writeAMF3Value(ba, value, references);
});
writeUTF8(ba, '', references);
for (let j = 0; j < densePortionLength; j++) {
writeAMF3Value(ba, array.axGetPublicProperty(j), references);
}
} else if (ba.sec.AXDate.axIsType(value)) {
ba.writeByte(AMF3Marker.DATE);
if (tryWriteAndStartTrackingReference(ba, value, references))
break;
writeU29(ba, 1);
writeDouble(ba, value.valueOf());
} else if (ba.sec.AXXML.axIsType(value)) {
ba.writeByte(AMF3Marker.XML);
writeUTF8(ba, value.toString(), references);
} else if (ba.sec.Int32Vector.axIsType(value)) {
const vector = <Float64Vector>value;
ba.writeByte(AMF3Marker.VECTOR_INT);
if (tryWriteAndStartTrackingReference(ba, vector, references)) {
break;
}
writeU29(ba, (vector.length << 1) | 1);
ba.writeUnsignedInt(+vector.fixed);
for (let i = 0; i < vector.length; i++) {
writeU29(ba, vector.axGetPublicProperty(i));
}
} else if (ba.sec.Uint32Vector.axIsType(value)) {
const vector = <Uint32Vector>value;
ba.writeByte(AMF3Marker.VECTOR_UINT);
if (tryWriteAndStartTrackingReference(ba, vector, references)) {
break;
}
writeU29(ba, (vector.length << 1) | 1);
ba.writeUnsignedInt(+vector.fixed);
for (let i = 0; i < vector.length; i++) {
writeU29(ba, vector.axGetPublicProperty(i));
}
} else if (ba.sec.Float64Vector.axIsType(value)) {
const vector = <Float64Vector>value;
ba.writeByte(AMF3Marker.VECTOR_DOUBLE);
if (tryWriteAndStartTrackingReference(ba, vector, references)) {
break;
}
writeU29(ba, (vector.length << 1) | 1);
ba.writeUnsignedInt(+vector.fixed);
for (let i = 0; i < vector.length; i++) {
writeDouble(ba, vector.axGetPublicProperty(i));
}
} else if (ba.sec.ObjectVector.axIsType(value)) {
const vector = <GenericVector>value;
ba.writeByte(AMF3Marker.VECTOR_OBJECT);
if (tryWriteAndStartTrackingReference(ba, vector, references)) {
break;
}
writeU29(ba, (vector.length << 1) | 1);
ba.writeUnsignedInt(+vector.fixed);
writeUTF8(ba, ba.sec.classAliases.getAliasByClass(value.axClass.type) || '*', references);
for (let i = 0; i < vector.length; i++) {
writeAMF3Value(ba, vector.axGetPublicProperty(i), references);
}
} else {
const object = <ASObject>value;
// TODO Dictionary, ByteArray
ba.writeByte(AMF3Marker.OBJECT);
if (tryWriteAndStartTrackingReference(ba, object, references)) {
break;
}
let isDynamic = true;
const axClass: AXClass = object.axClass;
if (axClass) {
const classInfo = axClass.classInfo;
isDynamic = !classInfo.instanceInfo.isSealed();
const alias = ba.sec.classAliases.getAliasByClass(axClass) || '';
const traitsRef = references.traits.indexOf(axClass);
let traitNames: string [] = null;
if (traitsRef < 0) {
// Write traits since we haven't done so yet.
traitNames = classInfo.instanceInfo.runtimeTraits.getPublicTraitNames();
references.traits.push(axClass);
references.traitNames.push(traitNames);
writeU29(ba, (isDynamic ? 0x0B : 0x03) + (traitNames.length << 4));
writeUTF8(ba, alias, references);
// Write trait names.
for (let i = 0; i < traitNames.length; i++) {
writeUTF8(ba, traitNames[i], references);
}
} else {
// Write a reference to the previously written traits.
traitNames = references.traitNames[traitsRef];
writeU29(ba, 0x01 + (traitsRef << 2));
}
// Write the actual trait values.
for (let i = 0; i < traitNames.length; i++) {
writeAMF3Value(ba, object.axGetPublicProperty(traitNames[i]), references);
}
} else {
// REDUX: I don't understand in what situations we wouldn't have a class definition, ask Yury.
// object with no class definition
writeU29(ba, 0x0B);
writeUTF8(ba, '', references); // empty alias name
}
// Write dynamic properties.
if (isDynamic) {
forEachPublicProperty(object, function (i, value) {
writeUTF8(ba, i, references);
writeAMF3Value(ba, value, references);
});
writeUTF8(ba, '', references);
}
}
return;
}
}
export class AMF3 {
public static write(ba: ByteArray, object: ASObject) {
writeAMF3Value(ba, object, new AMF3ReferenceTables());
}
public static read(ba: ByteArray) {
return readAMF3Value(ba, new AMF3ReferenceTables());
}
}