// Minimal AMF0 codec + Flash Remoting packet format (what mx.remoting / NetServices speaks). // Reference: Adobe "AMF0 File Format Specification" + the AMF remoting packet envelope. // // .NET mapping: // number (0x00) <-> double (any .NET numeric type is written as a double) // boolean (0x01) <-> bool // string (0x02 / 0x0C long) <-> string // object (0x03) <-> Dictionary (writer also accepts POCOs / anonymous objects) // null (0x05), undefined (0x06) -> null // reference(0x07) -> the previously read object/array // ecma arr (0x08) -> Dictionary // strict (0x0A) <-> List (writer accepts any IEnumerable that isn't a string) // date (0x0B) <-> DateTime (UTC) // xml (0x0F) -> string // typed obj(0x10) -> Dictionary with the class name under "__className" using System.Buffers.Binary; using System.Collections; using System.Reflection; using System.Text; namespace ReboundEmulator.Amf; public sealed record AmfHeader(string Name, bool MustUnderstand, object? Value); public sealed record AmfMessage(string Target, string Response, object? Body); public sealed class AmfPacket { public ushort Version { get; set; } // Flash 8 / AS2 remoting sends 0 public List Headers { get; } = new(); public List Messages { get; } = new(); public static AmfPacket Read(byte[] data) { var r = new Amf0Reader(data); var p = new AmfPacket { Version = r.U16() }; int headerCount = r.U16(); for (int i = 0; i < headerCount; i++) { string name = r.Utf8(r.U16()); bool must = r.U8() != 0; r.U32(); // byte length (may be 0xFFFFFFFF = unknown); not needed r.ResetReferences(); // each header/body has its own reference table p.Headers.Add(new AmfHeader(name, must, r.ReadValue())); } int messageCount = r.U16(); for (int i = 0; i < messageCount; i++) { string target = r.Utf8(r.U16()); // e.g. "com.pogo.flash.FlashRoomsFeed.generateFeed" string response = r.Utf8(r.U16()); // e.g. "/1" r.U32(); r.ResetReferences(); p.Messages.Add(new AmfMessage(target, response, r.ReadValue())); } return p; } public byte[] Write() { var w = new Amf0Writer(); w.U16(Version); w.U16((ushort)Headers.Count); foreach (var h in Headers) { w.ShortString(h.Name); w.U8((byte)(h.MustUnderstand ? 1 : 0)); WriteSized(w, h.Value); } w.U16((ushort)Messages.Count); foreach (var m in Messages) { w.ShortString(m.Target); // e.g. "/1/onResult" w.ShortString(m.Response); // "null" for replies WriteSized(w, m.Body); } return w.ToArray(); } // Values are prefixed with their byte length, so encode separately first. static void WriteSized(Amf0Writer w, object? value) { var inner = new Amf0Writer(); inner.WriteValue(value); var bytes = inner.ToArray(); w.U32((uint)bytes.Length); w.Raw(bytes); } } public sealed class Amf0Reader { readonly byte[] _d; int _p; readonly List _refs = new(); public Amf0Reader(byte[] data) => _d = data; public void ResetReferences() => _refs.Clear(); public byte U8() => _d[_p++]; public ushort U16() { var v = BinaryPrimitives.ReadUInt16BigEndian(_d.AsSpan(_p)); _p += 2; return v; } public uint U32() { var v = BinaryPrimitives.ReadUInt32BigEndian(_d.AsSpan(_p)); _p += 4; return v; } double F64() { var v = BinaryPrimitives.ReadDoubleBigEndian(_d.AsSpan(_p)); _p += 8; return v; } public string Utf8(int len) { var s = Encoding.UTF8.GetString(_d, _p, len); _p += len; return s; } public object? ReadValue() { byte marker = U8(); switch (marker) { case 0x00: return F64(); case 0x01: return U8() != 0; case 0x02: return Utf8(U16()); case 0x03: { var o = new Dictionary(); _refs.Add(o); ReadProps(o); return o; } case 0x05: return null; // null case 0x06: return null; // undefined case 0x07: return _refs[U16()]; // reference case 0x08: // ECMA (associative) array { U32(); // approximate count; ignore var o = new Dictionary(); _refs.Add(o); ReadProps(o); return o; } case 0x0A: // strict array { uint n = U32(); var list = new List((int)Math.Min(n, 4096)); _refs.Add(list); for (uint i = 0; i < n; i++) list.Add(ReadValue()); return list; } case 0x0B: // date: ms since epoch + tz (ignored) { double ms = F64(); U16(); return DateTime.UnixEpoch.AddMilliseconds(ms); } case 0x0C: return Utf8((int)U32()); // long string case 0x0F: return Utf8((int)U32()); // XML document (as text) case 0x10: // typed object { string cls = Utf8(U16()); var o = new Dictionary { ["__className"] = cls }; _refs.Add(o); ReadProps(o); return o; } case 0x11: throw new NotSupportedException("AMF3 value inside AMF0 (Flash 9+ only; AS2 never sends this)"); default: throw new FormatException($"Unknown AMF0 marker 0x{marker:X2} at offset {_p - 1}"); } } // key/value pairs until an empty key followed by the object-end marker 0x09 void ReadProps(Dictionary o) { while (true) { string key = Utf8(U16()); if (key.Length == 0 && _d[_p] == 0x09) { _p++; return; } o[key] = ReadValue(); } } } public sealed class Amf0Writer { readonly MemoryStream _s = new(); public byte[] ToArray() => _s.ToArray(); public void Raw(byte[] b) => _s.Write(b); public void U8(byte v) => _s.WriteByte(v); public void U16(ushort v) { Span b = stackalloc byte[2]; BinaryPrimitives.WriteUInt16BigEndian(b, v); _s.Write(b); } public void U32(uint v) { Span b = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(b, v); _s.Write(b); } void F64(double v) { Span b = stackalloc byte[8]; BinaryPrimitives.WriteDoubleBigEndian(b, v); _s.Write(b); } public void ShortString(string s) { var b = Encoding.UTF8.GetBytes(s); if (b.Length > ushort.MaxValue) throw new ArgumentException("string too long for a short string"); U16((ushort)b.Length); _s.Write(b); } public void WriteValue(object? v) { switch (v) { case null: U8(0x05); return; case bool b: U8(0x01); U8((byte)(b ? 1 : 0)); return; case string s: { var bytes = Encoding.UTF8.GetBytes(s); if (bytes.Length <= ushort.MaxValue) { U8(0x02); U16((ushort)bytes.Length); } else { U8(0x0C); U32((uint)bytes.Length); } _s.Write(bytes); return; } case byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal: U8(0x00); F64(Convert.ToDouble(v)); return; case Enum e: U8(0x00); F64(Convert.ToDouble(e)); return; case DateTime dt: U8(0x0B); F64((dt.ToUniversalTime() - DateTime.UnixEpoch).TotalMilliseconds); U16(0); return; case IDictionary dict: U8(0x03); foreach (DictionaryEntry kv in dict) { ShortString(kv.Key.ToString()!); WriteValue(kv.Value); } EndObject(); return; case IEnumerable list: { var items = list.Cast().ToList(); U8(0x0A); U32((uint)items.Count); foreach (var item in items) WriteValue(item); return; } default: // POCO / anonymous object -> anonymous AMF object U8(0x03); foreach (var prop in v.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (prop.GetIndexParameters().Length != 0) continue; ShortString(prop.Name); WriteValue(prop.GetValue(v)); } EndObject(); return; } } void EndObject() { U16(0); U8(0x09); } }