@astronautlabs/amf
Version:
Action Message Format (AMF0/3)
520 lines (417 loc) • 15.6 kB
text/typescript
import { BitstreamElement, Field, Variant, Serializer, BitstreamReader, BitstreamWriter, FieldDefinition, resolveLength, BufferedWritable, BooleanSerializer, Reserved, SerializeOptions, ReservedLow, BitstreamMeasurer, VariantMarker, IncompleteReadResult } from "@astronautlabs/bitstream";
import * as AMF3 from './amf3';
export enum TypeMarker {
Number = 0x00,
Boolean = 0x01,
String = 0x02,
Object = 0x03,
MovieClip = 0x04,
Null = 0x05,
Undefined = 0x06,
Reference = 0x07, // x
EcmaArray = 0x08,
ObjectEnd = 0x09,
StrictArray = 0x0A,
Date = 0x0B,
LongString = 0x0C,
Unsupported = 0x0D,
RecordSet = 0x0E,
XmlDocument = 0x0F,
TypedObject = 0x10, // x
AvmPlus = 0x11 // x
}
export class Value<T = any> extends BitstreamElement {
private _referenceTable : ComplexValue[];
get referenceTable() {
if (!this._referenceTable) {
if (this.context) {
if (this.context.__references) {
this._referenceTable = this.context.__references;
} else {
this.context.__references = this._referenceTable = [];
}
}
}
return this._referenceTable;
}
marker : TypeMarker;
get value(): T { return undefined; }
set value(value) {
throw new Error(`Cannot set value [${this.constructor.name}]`);
}
static get undefined() {
return new UndefinedValue();
}
static get true() {
return this.boolean(true);
}
static get false() {
return this.boolean(false);
}
static boolean(value : boolean) {
return new BooleanValue().with({ value });
}
static number(value : number) {
return new NumberValue().with({ value });
}
static string(value : string) {
if (value.length > 0xFFFF)
return new LongStringValue().with({ value });
else
return new StringValue().with({ value });
}
static xmlDocument(value : string) {
return new XmlDocumentValue().with({ value });
}
static object(value : object, className? : string) {
if (value === null || value === void 0 || typeof value !== 'object')
throw new Error(`Invalid value for object: ${value}`);
if (className)
return new TypedObjectValue().with({ value, className });
else
return new ObjectValue().with({ value });
}
static get null() {
return new NullValue();
}
static date(value : Date) {
return new DateValue().with({ value });
}
static array(value : any[]) {
return new StrictArrayValue().with({ value });
}
static associativeArray(value : Map<string, any> | Record<string,any>) {
if (value instanceof Map)
return new EcmaArrayValue().with({ value });
else
return new EcmaArrayValue().with({ value: new Map(Object.keys(value).map(key => [key, value[key]])) });
}
static any(value : any) {
if (value instanceof Value)
return value;
if (value === void 0)
return this.undefined;
if (value === true)
return this.true;
if (value === false)
return this.false;
if (value === null)
return this.null;
if (typeof value === 'number')
return this.number(value);
if (typeof value === 'string')
return this.string(value);
if (value instanceof Date)
return this.date(value);
if (value instanceof Map)
return this.associativeArray(value);
if (Array.isArray(value))
return this.array(value);
if (typeof value === 'object')
return this.object(value);
}
static avmPlus(value) {
return new AvmPlusValue().with({ value });
}
static amf3(value) {
return this.avmPlus(value);
}
}
export class ObjectProperty extends BitstreamElement {
keyLength : number;
private _key : string;
get key(): string { return this._key; }
set key(value) {
if (typeof value !== 'string')
throw new Error(`Key must be a string`);
this._key = value;
this.keyLength = value.length;
}
value : Value;
}
export class ObjectPropertyArraySerializer implements Serializer {
*read(reader: BitstreamReader, type: any, parent: BitstreamElement, field: FieldDefinition): Generator<IncompleteReadResult, any> {
let properties : ObjectProperty[] = [];
let readOperation : Generator<IncompleteReadResult, ObjectProperty>;
let hasMore = true;
while (hasMore) {
readOperation = ObjectProperty.read(reader);
while (true) {
let propResult = readOperation.next();
if (propResult.done === false) {
yield propResult.value;
} else {
let prop = propResult.value;
if (prop.value instanceof ObjectEndValue)
hasMore = false;
else
properties.push(prop);
break;
}
}
}
return properties;
}
write(writer: BitstreamWriter, type: any, parent: BitstreamElement, field: FieldDefinition, value: ObjectProperty[]) {
for (let prop of value)
prop.write(writer);
new ObjectProperty()
.with({ key: "", value: new ObjectEndValue() })
.write(writer)
;
}
}
<Value>(i => i.marker === TypeMarker.Number)
export class NumberValue extends Value {
marker = TypeMarker.Number;
private $value : number;
get value() { return this.$value; }
set value(value) { this.$value = value; }
}
<Value>(i => i.marker === TypeMarker.Boolean)
export class BooleanValue extends Value {
marker = TypeMarker.Boolean;
private $value : boolean;
get value() { return this.$value; }
set value(value) { this.$value = value; }
}
<Value>(i => i.marker === TypeMarker.String)
export class StringValue extends Value {
marker = TypeMarker.String;
length : number = 0;
private $value : string;
get value() { return this.$value; }
set value(value) {
if (typeof value !== 'string')
throw new Error(`Value must be a string`);
this.$value = value;
this.length = value.length;
}
}
<Value>(i => [TypeMarker.Object, TypeMarker.TypedObject, TypeMarker.StrictArray, TypeMarker.EcmaArray].includes(i.marker))
export class ComplexValue<T = any> extends Value<T> {
write(bitstream: BitstreamWriter, options?: SerializeOptions): void {
this.context = options?.context ?? {};
let json = JSON.stringify(this);
if (!this.context.id)
this.context.id = Math.floor(Math.random() * 1000);
let index = this.referenceTable?.findIndex?.(x => JSON.stringify(x) === json);
if (index >= 0) {
new ReferenceValue().with({ index }).write(bitstream, options);
} else {
super.write(bitstream, options);
}
}
onSerializeFinished(): void {
this.referenceTable.push(this);
}
onParseFinished(): void {
this.referenceTable.push(this);
}
}
function symbol(): symbol {
return Symbol('');
}
<Value>(i => [TypeMarker.Object, TypeMarker.TypedObject].includes(i.marker))
export class ObjectValue extends ComplexValue {
marker = TypeMarker.Object;
private _properties : ObjectProperty[];
$objectVariantMarker;
get properties() : ObjectProperty[] {
return this._properties;
}
set properties(value) {
this._properties = value;
this.buildValue();
}
private buildValue() {
this._value = this.properties.reduce((o, p) => (o[p.key] = p.value.value, o), {});
}
private _value : any;
get value() {
return this._value;
}
set value(value) {
this._value = value;
this._properties = Object
.keys(value ?? {})
.map(key => new ObjectProperty().with({
key,
value: Value.any(value[key])
}));
}
}
<ObjectValue>(i => i.marker === TypeMarker.TypedObject)
export class TypedObjectValue extends ObjectValue {
marker = TypeMarker.TypedObject;
private _className : string;
classNameLength : number;
get className() : string {
return this._className;
}
set className(value) {
if (typeof value !== 'string')
throw new TypeError(`Class name must be a string`);
this._className = value;
this.classNameLength = value.length;
}
}
<Value>(i => i.marker === TypeMarker.Null)
export class NullValue extends Value<null> {
marker = TypeMarker.Null;
}
<Value>(i => i.marker === TypeMarker.Undefined)
export class UndefinedValue extends Value<undefined> {
marker = TypeMarker.Undefined;
}
<Value>(i => i.marker === TypeMarker.Reference)
export class ReferenceValue extends Value {
marker = TypeMarker.Reference;
index : number;
get reference() {
return this.referenceTable?.[this.index];
}
get value() {
return this.reference?.value;
}
}
<Value>(i => i.marker === TypeMarker.EcmaArray)
export class EcmaArrayValue<V = any> extends ComplexValue<Map<string, V>> {
marker = TypeMarker.EcmaArray;
private _count : number;
private _properties : ObjectProperty[];
private get properties() : ObjectProperty[] {
return this._properties;
}
private endKey = '';
private endMarker = TypeMarker.ObjectEnd;
private set properties(value) {
this._properties = value;
this._value = new Map(value.map(prop => [prop.key, prop.value.value]));
}
private _value : Map<string, V>;
get value() {
return this._value;
}
set value(value) {
this._value = value;
this._properties = Array.from(value.entries()).map(([key, value]) => new ObjectProperty().with({ key, value: Value.any(value) }));
this._count = this._properties.length;
}
}
<Value>(i => i.marker === TypeMarker.ObjectEnd)
export class ObjectEndValue extends Value {
marker = TypeMarker.ObjectEnd;
}
<Value>(i => i.marker === TypeMarker.StrictArray)
export class StrictArrayValue<T = any> extends ComplexValue<T[]> {
marker = TypeMarker.StrictArray;
private _value : T[];
private _values : Value[];
count : number;
private get values() : Value[] {
return this._values;
}
private set values(value) {
this._values = value;
this._value = value.map(z => z.value);
}
get value() {
return this._value;
}
static elementValues(value : StrictArrayValue) {
return value.values;
}
set value(value) {
if (!Array.isArray(value))
throw new Error(`Value must be an array`);
this._value = value;
this.values = value.map(z => Value.any(z));
this.count = value.length;
}
}
<Value>(i => i.marker === TypeMarker.Date)
export class DateValue extends Value<Date> {
marker = TypeMarker.Date;
private _value : number;
private get $value() : number {
return this._value;
}
private set $value(value) {
this._value = Math.floor(value);
}
private timeZone : number;
get value() {
return new Date(this.$value);
}
set value(value) {
this.$value = value.getTime();
}
}
<Value>(i => i.marker === TypeMarker.LongString)
export class LongStringValue extends Value {
marker = TypeMarker.LongString;
private _length : number;
private $value : string;
get value() { return this.$value; }
set value(value) {
if (typeof value !== 'string')
throw new TypeError(`Value must be a string`);
this.$value = value;
this._length = value.length;
}
}
<Value>(i => i.marker === TypeMarker.Unsupported)
export class UnsupportedValue extends Value {
marker = TypeMarker.Unsupported;
}
<Value>(i => i.marker === TypeMarker.RecordSet)
export class RecordSetValue extends Value {
marker = TypeMarker.RecordSet;
}
<Value>(i => i.marker === TypeMarker.MovieClip)
export class MovieClipValue extends Value {
marker = TypeMarker.MovieClip;
}
<Value>(i => i.marker === TypeMarker.XmlDocument)
export class XmlDocumentValue extends Value {
marker = TypeMarker.XmlDocument;
private _length : number;
private $value : string;
get value() { return this.$value; }
set value(value) {
if (typeof value !== 'string')
throw new TypeError(`Value must be a string`);
this.$value = value;
this._length = value.length;
}
}
<Value>(i => i.marker === TypeMarker.AvmPlus)
export class AvmPlusValue extends Value {
marker = TypeMarker.AvmPlus;
private $value : AMF3.Value;
get value() {
return this.$value?.value;
}
set value(value) {
this.$value = AMF3.Value.any(value);
}
static unwrap(v : AvmPlusValue) {
return v.$value;
}
}