using Lua.Internal; using Lua.Runtime; using System.Buffers; using System.Buffers.Binary; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; namespace Lua.CodeAnalysis.Compilation; [SuppressMessage("ReSharper", "MemberCanBePrivate.Local")] [StructLayout(LayoutKind.Sequential, Pack = 1)] unsafe struct Header { public static ReadOnlySpan LuaSignature => "\eLua"u8; public static ReadOnlySpan LuaTail => [0x19, 0x93, 0x0d, 0x0a, 0x1a, 0x0a]; public fixed byte Signature[4]; public byte Version, Format, Endianness, IntSize; public byte PointerSize, InstructionSize; public byte NumberSize, IntegralNumber; public fixed byte Tail[6]; public const int Size = 18; public Header(bool isLittleEndian) { fixed (byte* signature = Signature) { LuaSignature.CopyTo(new(signature, 4)); } Version = (Constants.VersionMajor << 4) | Constants.VersionMinor; Format = 0; Endianness = (byte)(isLittleEndian ? 1 : 0); IntSize = 4; PointerSize = (byte)sizeof(IntPtr); InstructionSize = 4; NumberSize = 8; IntegralNumber = 0; fixed (byte* tail = Tail) { LuaTail.CopyTo(new(tail, 6)); } } public void Validate(ReadOnlySpan name) { fixed (byte* signature = Signature) { if (!LuaSignature.SequenceEqual(new(signature, 4))) { throw new LuaUndumpException($"{name.ToString()}: is not a precompiled chunk"); } } var major = Version >> 4; var minor = Version & 0xF; if (major != Constants.VersionMajor || minor != Constants.VersionMinor) { throw new LuaUndumpException($"{name.ToString()}: version mismatch in precompiled chunk {major}.{minor} != {Constants.VersionMajor}.{Constants.VersionMinor}"); } if (IntSize != 4 || Format != 0 || IntegralNumber != 0 || PointerSize is not (4 or 8) || InstructionSize != 4 || NumberSize != 8) { goto ErrIncompatible; } fixed (byte* tail = Tail) { if (!LuaTail.SequenceEqual(new(tail, 6))) { goto ErrIncompatible; } } return; ErrIncompatible: throw new LuaUndumpException($"{name.ToString()}: incompatible precompiled chunk"); } } unsafe ref struct DumpState(IBufferWriter writer, bool reversedEndian) { public readonly IBufferWriter Writer = writer; Span unWritten; void Write(ReadOnlySpan span) { var toWrite = span; var remaining = unWritten.Length; if (span.Length > remaining) { span[..remaining].CopyTo(unWritten); Writer.Advance(remaining); toWrite = span[remaining..]; unWritten = Writer.GetSpan(toWrite.Length); } toWrite.CopyTo(unWritten); Writer.Advance(toWrite.Length); unWritten = unWritten[toWrite.Length..]; } public bool IsReversedEndian => reversedEndian; void DumpHeader() { Header header = new(BitConverter.IsLittleEndian ^ IsReversedEndian); Write(new(&header, Header.Size)); } public void Dump(Prototype prototype) { if (unWritten.Length == 0) { unWritten = Writer.GetSpan(Header.Size + 32); } DumpHeader(); DumpFunction(prototype); } void DumpFunction(Prototype prototype) { WriteInt(prototype.LineDefined); // 4 WriteInt(prototype.LastLineDefined); // 4 WriteByte((byte)prototype.ParameterCount); // 1 WriteByte((byte)prototype.MaxStackSize); // 1 WriteByte((byte)(prototype.HasVariableArguments ? 1 : 0)); // 1 WriteIntSpanWithLength(MemoryMarshal.Cast(prototype.Code)); // 4 WriteConstants(prototype.Constants); // 4 WritePrototypes(prototype.ChildPrototypes); // 4 WriteUpValues(prototype.UpValues); // 4 // Debug WriteString(prototype.ChunkName); WriteIntSpanWithLength(prototype.LineInfo); WriteLocalVariables(prototype.LocalVariables); WriteInt(prototype.UpValues.Length); foreach (var desc in prototype.UpValues) { WriteString(desc.Name); } } void WriteInt(int v) { if (reversedEndian) { v = BinaryPrimitives.ReverseEndianness(v); } Write(new(&v, sizeof(int))); } void WriteLong(long v) { if (reversedEndian) { v = BinaryPrimitives.ReverseEndianness(v); } Write(new(&v, sizeof(long))); } void WriteByte(byte v) { Write(new(&v, sizeof(byte))); } void WriteDouble(double v) { var l = BitConverter.DoubleToInt64Bits(v); WriteLong(l); } void WriteIntSpanWithLength(ReadOnlySpan v) { WriteInt(v.Length); if (IsReversedEndian) { foreach (var i in v) { var reversed = BinaryPrimitives.ReverseEndianness(i); Write(new(&reversed, 4)); } } else { Write(MemoryMarshal.Cast(v)); } } void WriteBool(bool v) { WriteByte(v ? (byte)1 : (byte)0); } void WriteString(string v) { var bytes = Encoding.UTF8.GetBytes(v); var len = bytes.Length; if (bytes.Length != 0) { len++; } if (sizeof(IntPtr) == 8) { WriteLong(len); } else { WriteInt(len); } if (len != 0) { Write(bytes); WriteByte(0); } } void WriteConstants(ReadOnlySpan constants) { WriteInt(constants.Length); foreach (var c in constants) { WriteByte((byte)c.Type); switch (c.Type) { case LuaValueType.Nil: break; case LuaValueType.Boolean: WriteBool(c.UnsafeReadDouble() != 0); break; case LuaValueType.Number: WriteDouble(c.UnsafeReadDouble()); break; case LuaValueType.String: WriteString(c.UnsafeRead()); break; } } } void WritePrototypes(ReadOnlySpan prototypes) { WriteInt(prototypes.Length); foreach (var p in prototypes) { DumpFunction(p); } } void WriteLocalVariables(ReadOnlySpan localVariables) { WriteInt(localVariables.Length); foreach (var v in localVariables) { WriteString(v.Name); WriteInt(v.StartPc); WriteInt(v.EndPc); } } void WriteUpValues(ReadOnlySpan upValues) { WriteInt(upValues.Length); foreach (var u in upValues) { WriteBool(u.IsLocal); WriteByte((byte)u.Index); } } } unsafe ref struct UndumpState(ReadOnlySpan span, ReadOnlySpan name, StringInternPool internPool) { public ReadOnlySpan Unread = span; bool otherEndian; int pointerSize; readonly ReadOnlySpan name = name; void Throw(string why) { throw new LuaUndumpException($"{name.ToString()}: {why} precompiled chunk"); } void ThrowTooShort() { Throw("truncate"); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void Read(Span dst) { if (Unread.Length < dst.Length) { ThrowTooShort(); } Unread[..dst.Length].CopyTo(dst); Unread = Unread[dst.Length..]; } byte ReadByte() { if (0 < Unread.Length) { var b = Unread[0]; Unread = Unread[1..]; return b; } ThrowTooShort(); return 0; } bool ReadBool() { if (0 < Unread.Length) { var b = Unread[0]; Unread = Unread[1..]; return b != 0; } ThrowTooShort(); return false; } int ReadInt() { var i = 0; Span span = new(&i, sizeof(int)); Read(span); if (otherEndian) { i = BinaryPrimitives.ReverseEndianness(i); } return i; } long ReadLong() { long i = 0; Span span = new(&i, sizeof(long)); Read(span); if (otherEndian) { i = BinaryPrimitives.ReverseEndianness(i); } return i; } double ReadDouble() { var i = ReadLong(); return *(double*)&i; } public Prototype Undump() { Header h = default; Span span = new(&h, sizeof(Header)); Read(span); h.Validate(name); otherEndian = BitConverter.IsLittleEndian ^ (h.Endianness == 1); pointerSize = h.PointerSize; return UndumpFunction(); } Prototype UndumpFunction() { var lineDefined = ReadInt(); // 4 var lastLineDefined = ReadInt(); // 4 var parameterCount = ReadByte(); // 1 var maxStackSize = ReadByte(); // 1 var isVarArg = ReadByte() == 1; // 1 var codeLength = ReadInt(); var code = new Instruction[codeLength]; ReadInToIntSpan(MemoryMarshal.Cast(code)); var constants = ReadConstants(); var prototypes = ReadPrototypes(); var upValues = ReadUpValues(); // Debug var source = ReadString(); var lineInfoLength = ReadInt(); var lineInfo = new int[lineInfoLength]; ReadInToIntSpan(lineInfo.AsSpan()); var localVariables = ReadLocalVariables(); var upValueCount = ReadInt(); Debug.Assert(upValueCount == upValues.Length, $"upvalue count mismatch: {upValueCount} != {upValues.Length}"); foreach (ref var desc in upValues.AsSpan()) { var name = ReadString(); desc.Name = name; } return new(source, lineDefined, lastLineDefined, parameterCount, maxStackSize, isVarArg, constants, code, prototypes, lineInfo, localVariables, upValues); } void ReadInToIntSpan(Span toWrite) { for (var i = 0; i < toWrite.Length; i++) { toWrite[i] = ReadInt(); } } string ReadString() { var len = pointerSize == 4 ? ReadInt() : (int)ReadLong(); if (len == 0) { return ""; } len--; var arrayPooled = ArrayPool.Shared.Rent(len); char[]? charArrayPooled = null; try { var span = arrayPooled.AsSpan(0, len); Read(span); var l = ReadByte(); Debug.Assert(l == 0); var chars = len <= 128 ? stackalloc char[len * 2] : (charArrayPooled = ArrayPool.Shared.Rent(len * 2)); var count = Encoding.UTF8.GetChars(span, chars); return internPool.Intern(chars[..count]); } finally { ArrayPool.Shared.Return(arrayPooled); if (charArrayPooled != null) { ArrayPool.Shared.Return(charArrayPooled); } } } LuaValue[] ReadConstants() { var count = ReadInt(); var constants = new LuaValue[count]; for (var i = 0; i < count; i++) { var type = (LuaValueType)ReadByte(); switch (type) { case LuaValueType.Nil: break; case LuaValueType.Boolean: constants[i] = ReadByte() == 1; break; case LuaValueType.Number: constants[i] = ReadDouble(); break; case LuaValueType.String: constants[i] = ReadString(); break; } } return constants; } Prototype[] ReadPrototypes() { var count = ReadInt(); var prototypes = count != 0 ? new Prototype[count] : []; for (var i = 0; i < count; i++) { prototypes[i] = UndumpFunction(); } return prototypes; } LocalVariable[] ReadLocalVariables() { var count = ReadInt(); var localVariables = new LocalVariable[count]; for (var i = 0; i < count; i++) { var name = ReadString(); var startPc = ReadInt(); var endPc = ReadInt(); localVariables[i] = new() { Name = name, StartPc = startPc, EndPc = endPc }; } return localVariables; } UpValueDesc[] ReadUpValues() { var count = ReadInt(); Debug.Assert(count < 100, $" too many upvalues :{count}"); var upValues = new UpValueDesc[count]; for (var i = 0; i < count; i++) { var isLocal = ReadBool(); var index = ReadByte(); upValues[i] = new() { IsLocal = isLocal, Index = index }; } return upValues; } }