222 lines
9.0 KiB
C#
222 lines
9.0 KiB
C#
// 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<string, object?> (writer also accepts POCOs / anonymous objects)
|
|
// null (0x05), undefined (0x06) -> null
|
|
// reference(0x07) -> the previously read object/array
|
|
// ecma arr (0x08) -> Dictionary<string, object?>
|
|
// strict (0x0A) <-> List<object?> (writer accepts any IEnumerable that isn't a string)
|
|
// date (0x0B) <-> DateTime (UTC)
|
|
// xml (0x0F) -> string
|
|
// typed obj(0x10) -> Dictionary<string, object?> 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<AmfHeader> Headers { get; } = new();
|
|
public List<AmfMessage> 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<object> _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<string, object?>(); _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<string, object?>(); _refs.Add(o); ReadProps(o); return o;
|
|
}
|
|
case 0x0A: // strict array
|
|
{
|
|
uint n = U32();
|
|
var list = new List<object?>((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<string, object?> { ["__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<string, object?> 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<byte> b = stackalloc byte[2]; BinaryPrimitives.WriteUInt16BigEndian(b, v); _s.Write(b); }
|
|
public void U32(uint v) { Span<byte> b = stackalloc byte[4]; BinaryPrimitives.WriteUInt32BigEndian(b, v); _s.Write(b); }
|
|
void F64(double v) { Span<byte> 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<object?>().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); }
|
|
}
|