UNPKG

@astronautlabs/amf

Version:

Action Message Format (AMF0/3)

945 lines (747 loc) 29.1 kB
import { BitstreamElement, BitstreamReader, BitstreamWriter, DefaultVariant, Field, FieldDefinition, Marker, Serializer, Variant, VariantMarker } from "@astronautlabs/bitstream"; import { U29Serializer } from "./u29"; // 00100011 export enum TypeMarker { Undefined = 0x00, Null = 0x01, False = 0x02, True = 0x03, Integer = 0x04, Double = 0x05, String = 0x06, XmlDocument = 0x07, Date = 0x08, Array = 0x09, Object = 0x0A, Xml = 0x0B, ByteArray = 0x0C, VectorInt = 0x0D, VectorUint = 0x0E, VectorDouble = 0x0F, VectorObject = 0x10, Dictionary = 0x11 } export const REFERENCE_TYPES = [ TypeMarker.XmlDocument, TypeMarker.Date, TypeMarker.Array, TypeMarker.Object, TypeMarker.Xml, TypeMarker.ByteArray, TypeMarker.VectorDouble, TypeMarker.VectorInt, TypeMarker.VectorUint, TypeMarker.VectorObject, TypeMarker.Dictionary, TypeMarker.String ]; export function Type(type : Function) : PropertyDecorator { return (target, propertyKey) => { Reflect.defineMetadata('amf:type', type, target, propertyKey) }; } export function amfTypeForProperty(object : any, propertyKey : string): Constructor<Value> { let value = object[propertyKey]; if (object.prototype) { let declared = Reflect.getMetadata('amf:type', object.prototype); if (declared) return declared; } return amfTypeForValue(value); } export function amfValueForProperty(object : any, propertyKey : string): Value { if (object[propertyKey] instanceof Value) return object[propertyKey]; return new (amfTypeForProperty(object, propertyKey))().with({ value: object[propertyKey] }); } export type Constructor<T> = { new() : T }; export function amfTypeForValue(value): Constructor<Value> { if (typeof value === 'number') return DoubleValue; if (typeof value === 'undefined') return UndefinedValue; if (value === null) return NullValue; if (value === true) return TrueValue; if (value === false) return FalseValue; if (typeof value === 'string') return StringValue; if (value instanceof Date) return DateValue; if (value instanceof Buffer) return ByteArray; if (value instanceof Uint32Array) return UIntVectorValue; if (value instanceof Int32Array) return IntVectorValue; if (value instanceof Map) return DictionaryValue; if (Array.isArray(value)) return ArrayValue; if (typeof value === 'object') return ObjectValueWithLiteralTraits; } export class References { strings : StringValue[] = []; traits : Traits[] = []; values : Value[] = []; } /** * Represents an AMF3 "Value" in the binary protocol */ export class Value<T = any> extends BitstreamElement { get references(): References { if (!this.context.__references) this.context.__references = new References(); return this.context.__references; } @Field(8*1) marker : TypeMarker; get value() : T { return undefined; } set value(value) { throw new Error(`Value cannot be set on this type [${this.constructor.name}]`); } static get undefined() { return new UndefinedValue(); } static get null() { return new NullValue(); } static boolean(value : boolean) { if (value) return new TrueValue(); else return new FalseValue(); } static int(value : number) { return new IntegerValue().with({ value }); } static double(value : number) { return new DoubleValue().with({ value }); } static string(value : string) { return new StringValue().with({ value }); } static xmlDocument(value : string) { return new XmlDocumentValue().with({ value }); } static xml(value : string) { return new XmlValue().with({ value }); } static date(value : Date) { return new DateValue().with({ value }); } static any(value : any) : Value { return value instanceof Value ? value : new (amfTypeForValue(value))().with({ value }); } static array(array : any[]) : ArrayValue { let keys = Object.keys(array); let keySet = new Set(keys); let associativeKeySet = new Set(keys); let isSparse = false; let maxDenseKey : number = undefined; // Determine how dense the array is and which keys are associative. // This will include any keys that are not part of the dense portion // of the array for (let i = 0, max = array.length; i < max; ++i) { if (!keySet.has(i.toString())) { isSparse = true; continue; } if (!isSparse) { maxDenseKey = i; associativeKeySet.delete(i.toString()); } } return new ArrayValue().with({ values: array.slice(0, maxDenseKey + 1) .map(value => Value.any(value)), associativeValues: Array.from(associativeKeySet.values()) .map(key => new AssociativeValue().with({ key, value: Value.any(array[key]) })) }); } static object(value : object) { return new ObjectValueWithLiteralTraits().with({ value }); } static byteArray(buffer : Uint8Array | Buffer) { return new ByteArray().with({ value: Buffer.from(buffer) }); } static vector(value : Int32Array | Uint32Array | number[], isFixed = false) { if (value instanceof Int32Array) return new IntVectorValue().with({ value, isFixed }); if (value instanceof Uint32Array) return new UIntVectorValue().with({ value, isFixed }); if (Array.isArray(value)) { if (value.some(x => typeof x !== 'number')) { throw new TypeError( `Passing number[] to vector() produces a DoubleVector ` + `but the passed array has one or more elements which are ` + `not of type 'number'. Did you mean to use objectVector()?` ); } return new DoubleVectorValue().with({ value, isFixed }); } throw new TypeError(`The passed value cannot be converted to an Int32, Uint32 or Double AMF3 vector!`); } static objectVector(values : any[], isFixed = false) { return new ObjectVectorValue().with({ values: values.map(x => Value.any(x)), isFixed }); } static dictionary<K, V>(map : Map<K, V>): DictionaryValue<K, V> { return new DictionaryValue<K, V>().with({ value: map }); } } @Variant<Value>(i => i.marker === TypeMarker.Undefined) export class UndefinedValue extends Value<undefined> { marker = TypeMarker.Undefined; get value() { return undefined; } } @Variant<Value>(i => i.marker === TypeMarker.Null) export class NullValue extends Value<null> { marker = TypeMarker.Null; get value() { return null; } } @Variant<Value>(i => i.marker === TypeMarker.False) export class FalseValue extends Value<false> { marker = TypeMarker.False; get value() { return <false>false; } } @Variant<Value>(i => i.marker === TypeMarker.True) export class TrueValue extends Value<true> { marker = TypeMarker.True; get value() { return <true>true; } } @Variant<Value>(i => i.marker === TypeMarker.Integer) export class IntegerValue extends Value<number> { marker = TypeMarker.Integer; @Field(0, { serializer: new U29Serializer() }) $value : number; get value() { return this.$value; } set value(value) { this.$value = value; } } @Variant<Value>(i => i.marker === TypeMarker.Double) export class DoubleValue extends Value<number> { marker = TypeMarker.Double; @Field(8*8, { number: { format: 'float' } }) $value : number; get value() { return this.$value; } set value(value) { this.$value = value; } } export class StringOrReference extends BitstreamElement { @Field(0, { serializer: new U29Serializer() }) private $lengthOrReference : number; get isReference() { return !this.isLiteral; } get isLiteral() { return (this.$lengthOrReference & 0x1) === 1; } @Field((i : StringOrReference) => i.$lengthOrReference >> 1, { presentWhen: (i : StringOrReference) => !i.isReference }) private $value : string; get id() { return this.isReference ? this.$lengthOrReference >> 1 : undefined }; set id(value) { if (value > 0xFFFFFFF) throw new Error(`Maximum ID is 0xFFFFFFF (1114111)`); this.$lengthOrReference = (value << 1); this.$value = undefined; } get value() { return this.$value; } set value(value) { this.$lengthOrReference = (Buffer.from(value).length << 1) | 0x1; this.$value = value; } } @Variant<Value>(i => REFERENCE_TYPES.includes(i.marker)) export class ReferenceValue<T> extends Value<T> { } /** * Represents the "String" type of "Value" in Adobe's ActionScript Message Format (AMF) version 3. * - U29Serializer: Encodes/decodes values in AMF3's custom variable-length integer format * - Low bit of 0 or 1 on the "length" field determines if the value is a reference to a String Table entry * or is an inline string literal * - Thus IDs and lengths are limited to 2^28 since variable length strs are max 29 bits * * From the user's perspective, just set either `id` or `value`. When reading use `isReference()` to determine * whether this string is a reference to the string table or literal and `id` or `value` respectively. `id` and * `value` return `undefined` when they are not relevant for this object. */ @Variant<Value>(i => [TypeMarker.String, TypeMarker.XmlDocument, TypeMarker.Xml].includes(i.marker)) export class StringValue extends ReferenceValue<string> { marker = TypeMarker.String; @Field() stringOrReference : StringOrReference = new StringOrReference(); get isLiteral() { return this.stringOrReference.isLiteral; } get isReference() { return this.stringOrReference.isReference; } get id() { return this.stringOrReference.id; } set id(id) { this.stringOrReference.id = id; }; get value() { return this.stringOrReference.value; } set value(value) { this.stringOrReference.value = value; } } @Variant<StringValue>(i => i.marker === TypeMarker.XmlDocument) export class XmlDocumentValue extends StringValue { marker = TypeMarker.XmlDocument; } @Variant<StringValue>(i => i.marker === TypeMarker.Xml) export class XmlValue extends StringValue { marker = TypeMarker.Xml; } @Variant<Value>(i => i.marker === TypeMarker.Date) export class DateValue extends ReferenceValue<Date> { marker = TypeMarker.Date; @Field(0, { serializer: new U29Serializer() }) $indicator : number = 0x1; get isLiteral() { return this.$indicator !== 0; } get isReference() { return !this.isLiteral; } @Field(8*8, { number: { format: 'float' }, presentWhen: i => i.isLiteral }) private $value : number = 0; private $date : Date; get value() { return this.$date ??= new Date(this.$value); } set value(value) { if (value === null || value === void 0) throw new TypeError(`AMF3 cannot transport null/undefined Date`); if (!(value instanceof Date)) throw new TypeError(`Value must be a valid Date`); this.$date = value; this.$value = value.getTime(); } } export class AssociativeValueSerializer implements Serializer { *read(reader: BitstreamReader, type: any, parent: BitstreamElement, field: FieldDefinition) { let assocs : AssociativeValue[] = []; while (true) { let av = AssociativeValue.read(reader).next(); if (av.done === false) { yield av.value; return; } if (av.value.key === '') break; assocs.push(av.value); } return assocs; } write(writer: BitstreamWriter, type: any, parent: BitstreamElement, field: FieldDefinition, value: AssociativeValue[]) { value.forEach(a => a.write(writer)); new AssociativeValue().with({ key: '' }).write(writer); } } export class AssociativeValue extends BitstreamElement { @Field() private $key : StringOrReference; get key() { return this.$key.value; } set key(value) { this.$key.value = value; } @Field(0, { presentWhen: i => i.key !== '' }) value : Value; } @Variant<Value>(i => i.marker === TypeMarker.Array) export class ArrayValue<T = any> extends ReferenceValue<T[]> { marker = TypeMarker.Array; @Field(0, { serializer: new U29Serializer(), writtenValue: (i : ArrayValue) => i.isLiteral ? ((i.values.length << 1) | 0x1) : (i.id << 1) }) private $denseLengthOrReference : number; get isReference() { return !this.isLiteral; } get isLiteral() { return (this.$denseLengthOrReference & 0x1) === 1; } get id() { return this.isReference ? this.$denseLengthOrReference >> 1 : undefined }; set id(value) { if (value > 0xFFFFFFF) throw new Error(`Maximum ID is 0xFFFFFFF (1114111)`); this.$denseLengthOrReference = (value << 1); this.values = undefined; } get denseLength() { if (this.isLiteral) return this.values?.length ?? this.$denseLengthOrReference >> 1; } @Field(0, { array: { type: AssociativeValue }, serializer: new AssociativeValueSerializer() }) associativeValues : AssociativeValue[] = []; @Field(i => i.denseLength, { array: { type: Value }}) private $values : Value[]; get values() { return this.$values; } set values(value) { this.$values = value; this.$denseLengthOrReference = value.length << 1 | 0x1; } } export class ClassRegistry { private _map = new Map<string, Function>(); register(klass : Function, name? : string) { this._map.set(name ?? klass.name, klass); } get(name : string) { return this._map.get(name); } } @Variant<Value>(i => i.marker === TypeMarker.Object) export class ObjectValue extends ReferenceValue<object> { get registry(): ClassRegistry { // So this happens when creating the value outside of parsing. // TODO: Not clear this is the best behavior if (!this.context) return new ClassRegistry(); if (!this.context.__classes) this.context.__classes = new ClassRegistry(); return this.context.__classes; } marker = TypeMarker.Object; @Field(0, { serializer: new U29Serializer() }) $objectTypeIndicator : number; get isReference() { return !this.isLiteral; } get isLiteral() { return (this.$objectTypeIndicator & 0x1) === 1; } get isTraitLiteral() { return this.isLiteral && (this.$objectTypeIndicator & 0x2) === 2; } get isTraitReference() { return !this.isTraitLiteral; } get isExternalizable() { return this.isTraitLiteral && (this.$objectTypeIndicator & 0x4) === 0x4; } get id() { return this.isReference ? this.$objectTypeIndicator >> 1 : undefined; } set id(value) { this.$objectTypeIndicator = value << 1; } static reference(id : number) { return new ObjectValue().with({ id }); } @VariantMarker() $variantMarker; @Field(0, { array: { type: AssociativeValue, hasMore: (i : ObjectValue) => i.dynamicMembers.length === 0 || i.dynamicMembers[i.dynamicMembers.length - 1].key !== '' }, presentWhen: i => i.isDynamic, serializer: new AssociativeValueSerializer() }) private _dynamicMembers : AssociativeValue[] = []; get dynamicMembers() { return this._dynamicMembers; } set dynamicMembers(value) { this._dynamicMembers = value; } @Field((i : ObjectValueWithInternalTraits) => i.traits.sealedMemberNames.length, { array: { type: Value }}) private _values : Value[] = []; get values() { return this._values; } set values(value) { if (value === undefined || value === null) throw new TypeError(`Value cannot be set to undefined/null`); this._values = value; } } export class Traits extends BitstreamElement { @Field() className : StringOrReference; @Field((i : Traits) => i.parent.as(ObjectValueWithLiteralTraits).sealedMemberNameCount, { array: { type: StringOrReference } }) sealedMemberNames : StringOrReference[] = []; get isDynamic() { return this.parent.as(ObjectValueWithLiteralTraits).isDynamic; } } @Variant<ObjectValue>(i => !i.isExternalizable) export class ObjectValueWithInternalTraits extends ObjectValue { $objectTypeIndicator = 0b001; get traits() : Traits { return undefined; } private _value : object; get value() { return this._value; } set value(value) { this._value = value; } get values() { return super.values; } set values(value) { if (value === undefined || value === null) throw new TypeError(`Cannot assign null/undefined to ObjectValueWithInternalTraits.values`); super.values = value; this.buildValue(); } set dynamicMembers(value: AssociativeValue[]) { super.dynamicMembers = value; this.buildValue(); } protected buildValue() { if (!this.traits) { return; } let values = this.values ?? []; let obj : any; let klass : any = this.registry.get(this.traits.className.value); if (klass) { obj = new klass() } else { obj = {}; } this._value = this.traits.sealedMemberNames.reduce((o, name, i) => (o[name.value] = values[i]?.value, o), obj); if (this.dynamicMembers) this.dynamicMembers.forEach(m => this._value[m.key] = m.value); } onParseFinished(): void { this.buildValue(); } } @Variant<ObjectValue>(i => i.isTraitLiteral) export class ObjectValueWithLiteralTraits extends ObjectValueWithInternalTraits { $objectTypeIndicator = 0b011; get isDynamic() { return (this.$objectTypeIndicator & 0x8) === 0x8; } set isDynamic(value) { if (value) this.$objectTypeIndicator |= 0x8; else this.$objectTypeIndicator &= ~0x8; } get sealedMemberNameCount() { return (this.$objectTypeIndicator & 0x1ffffff0) >>> 4; } set sealedMemberNameCount(value) { this.$objectTypeIndicator &= 0xF; this.$objectTypeIndicator |= (value << 4); } @Field() private $traits : Traits; get traits() { return this.$traits; } set traits(value) { this.$traits = value; this.buildValue(); } get value() { return super.value; } set value(value) { let keys = Object.keys(value); this.traits = new Traits().with({ className: new StringOrReference().with({ value: '' }), sealedMemberNames: keys.map(x => new StringOrReference().with({ value: x })) }); this.sealedMemberNameCount = keys.length; let encoded = keys.map(key => amfValueForProperty(value, key)); this.values = encoded; this.buildValue(); } } @Variant<ObjectValue>(i => i.isTraitReference) export class ObjectValueWithReferencedTraits extends ObjectValueWithInternalTraits { $objectTypeIndicator = 0b001; get traitsId() { return this.$objectTypeIndicator & 0x7ffffff; } set traitsId(id : number) { this.$objectTypeIndicator &= 0x18000000; this.$objectTypeIndicator |= (id & 0x7ffffff); } get traits() { return this.references.traits[this.traitsId]; } set traits(value) { let index = this.references.traits.indexOf(value); if (index < 0) throw new Error(`The traits value ${value.toJSON()} is not in the traits index.`); this.traitsId = index; } } @Variant<ObjectValue>(i => i.isExternalizable) export class ObjectValueWithExternalizableTraits extends ObjectValue { $objectTypeIndicator = 0b111; @Field() className : StringOrReference; } @DefaultVariant() export class ObjectValueWithUnknownExternalizableTraits extends ObjectValueWithExternalizableTraits { constructor() { super(); throw new TypeError(`Unsupported externalizable object value`); } } @Variant<Value>(i => i.marker === TypeMarker.ByteArray) export class ByteArray extends ReferenceValue<Buffer> { marker = TypeMarker.ByteArray; @Field(0, { serializer: new U29Serializer(), writtenValue: i => i.value.length }) private $lengthOrReference : number; get isReference() { return !this.isLiteral; } get isLiteral() { return (this.$lengthOrReference & 0x1) === 1; } get id() { return this.isReference ? this.$lengthOrReference >> 1 : undefined; } set id(value) { this.$lengthOrReference = value << 1; } @Field(i => i.$length) $value : Buffer; get value() { return this.isLiteral ? this.$value : undefined; } set value(value) { this.$value = value; this.$lengthOrReference = value.length << 1 | 0x1 }; } function int32ArrayToBytes(array : Int32Array) { let buf = new Uint8Array(array.length * array.BYTES_PER_ELEMENT); let view = new DataView(buf.buffer); for (let i = 0, max = array.length; i < max; ++i) { view.setInt32(i*array.BYTES_PER_ELEMENT, array[i], false); } return buf; } function uint32ArrayToBytes(array : Uint32Array) { let buf = new Uint8Array(array.length * array.BYTES_PER_ELEMENT); let view = new DataView(buf.buffer); for (let i = 0, max = array.length; i < max; ++i) view.setUint32(i*array.BYTES_PER_ELEMENT, array[i], false); return buf; } function doubleArrayToBytes(array : number[]) { let buf = new Uint8Array(array.length * 8); let view = new DataView(buf.buffer); for (let i = 0, max = array.length; i < max; ++i) view.setFloat64(i*8, array[i], false); } function bytesToInt32Array(array : Uint8Array) { if (!array) return undefined; let buf = new Int32Array(array.length / 4); let view = new DataView(array.buffer); for (let i = 0, max = buf.length; i < max; ++i) { buf[i] = view.getInt32(i*buf.BYTES_PER_ELEMENT, false); } return buf; } function bytesToUint32Array(array : Uint8Array) { let buf = new Uint32Array(array.length / 4); let view = new DataView(array.buffer); for (let i = 0, max = buf.length; i < max; ++i) buf[i] = view.getUint32(i*buf.BYTES_PER_ELEMENT, false); return buf; } function bytesToDoubleArray(array : Uint8Array) { let buf = []; let view = new DataView(array.buffer); for (let i = 0, max = array.length / 8; i < max; ++i) buf[i] = view.getFloat64(i*8, false); return buf; } @Variant<Value>(i => [TypeMarker.VectorDouble, TypeMarker.VectorInt, TypeMarker.VectorObject, TypeMarker.VectorUint].includes(i.marker)) export class VectorValue<T = any> extends ReferenceValue<T> { @Field(0, { serializer: new U29Serializer(), writtenValue: i => i.value.length }) protected $lengthOrReference : number; get isReference() { return !this.isLiteral; } get isLiteral() { return (this.$lengthOrReference & 0x1) === 1; } get id() { return this.isReference ? this.$lengthOrReference >> 1 : undefined; } set id(value) { this.$lengthOrReference = value << 1; } get length() { return this.isLiteral ? this.$lengthOrReference >> 1 : undefined; } @Field(8, { presentWhen: i => i.isLiteral }) isFixed : boolean = true; } @Variant<VectorValue>(i => i.marker === TypeMarker.VectorObject) export class ObjectVectorValue extends VectorValue<object> { marker = TypeMarker.VectorObject; @Field(i => i.length, { array: { type: Value }}) values : Value[]; get value() { return this.values; } set value(value) { this.values = value; this.$lengthOrReference = value.length << 1 | 0x1; } @Field() objectTypeName : StringOrReference; } @Variant<VectorValue>(i => i.marker === TypeMarker.VectorDouble) export class DoubleVectorValue extends VectorValue<number[]> { marker = TypeMarker.VectorDouble; @Field(i => i.length, { array: { type: Number, elementLength: 8*4 }, number: { format: 'float' } }) values : number[]; get value() { return this.values; } set value(value) { this.values = value; this.$lengthOrReference = value.length << 1 | 0x1; } } @Variant<VectorValue>(i => i.marker === TypeMarker.VectorInt) export class IntVectorValue extends VectorValue<Int32Array> { marker = TypeMarker.VectorInt; private _bytes : Uint8Array; private _elements : Int32Array; @Field(i => i.length * 8 * 4) get bytes(): Uint8Array { return this._bytes; } set bytes(value) { this._bytes = value; this._elements = bytesToInt32Array(value); this.$lengthOrReference = this._elements.length << 1 | 0x1; } get value() { return this._elements; } set value(value) { this._elements = value; this._bytes = int32ArrayToBytes(value); this.$lengthOrReference = value.length << 1 | 0x1; } } @Variant<VectorValue>(i => i.marker === TypeMarker.VectorUint) export class UIntVectorValue extends VectorValue<Uint32Array> { marker = TypeMarker.VectorUint; private _bytes : Uint8Array; private _elements : Uint32Array; @Field(i => i.length * 8 * 4) get bytes(): Uint8Array { return this._bytes; } set bytes(value) { this._bytes = value; this._elements = bytesToUint32Array(value); this.$lengthOrReference = this._elements.length << 1 | 0x1; } get value() { return this._elements; } set value(value) { this._elements = value; this._bytes = uint32ArrayToBytes(value); this.$lengthOrReference = this._elements.length << 1 | 0x1; } } export class DictionaryEntry extends BitstreamElement { @Field() key : Value; @Field() value : Value; } @Variant<Value>(i => i.marker === TypeMarker.Dictionary) export class DictionaryValue<K = any, V = any> extends ReferenceValue<Map<K,V>> { marker = TypeMarker.Dictionary; @Field(0, { serializer: new U29Serializer(), writtenValue: i => i.value.length }) private $lengthOrReference : number; get isReference() { return !this.isLiteral; } get isLiteral() { return (this.$lengthOrReference & 0x1) === 1; } get id() { return this.isReference ? this.$lengthOrReference >> 1 : undefined; } set id(value) { this.$lengthOrReference = value << 1; } get length() { return this.isLiteral ? this.$lengthOrReference >> 1 : undefined; } @Field(8, { presentWhen: i => i.isLiteral }) hasWeakKeys : boolean; get value() { return new Map<K, V>(this.entries.map(e => [e.key.value, e.value.value])); } set value(value) { let entries : DictionaryEntry[] = []; value.forEach((v, k) => { entries.push(new DictionaryEntry().with({ key: Value.any(k), value: Value.any(v) })); }); this.entries = entries; } @Field(i => i.length, { presentWhen: i => i.isLiteral, array: { type: DictionaryEntry } }) private $entries : DictionaryEntry[]; get entries() { return this.$entries; } set entries(value) { this.$entries = value; this.$lengthOrReference = this.$entries.length << 1 | 0x1; } }