Initial commit

This commit is contained in:
2026-07-13 09:25:40 +07:00
parent c08ff54676
commit bccfb156d7
1938 changed files with 641646 additions and 0 deletions

View File

@@ -0,0 +1,296 @@
using System.Runtime.InteropServices;
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
internal static class Constants
{
internal const ushort IMAGE_FILE_MACHINE_I386 = 0x014c;
internal const ushort IMAGE_FILE_MACHINE_IA64 = 0x0200;
internal const ushort IMAGE_FILE_MACHINE_AMD64 = 0x8664;
}
/// <summary>
/// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_data_directory
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct IMAGE_DATA_DIRECTORY
{
public uint VirtualAddress; // DWORD VirtualAddress
public uint Size; // DWORD Size
}
/// <summary>
/// https://www.nirsoft.net/kernel_struct/vista/IMAGE_DOS_HEADER.html
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct IMAGE_DOS_HEADER
{
public ushort MagicNumber; // e_magic - Magic number (The value “MZ” are the initials of the PE designer Mark Zbikowski)
public ushort BytesOnLastPageOfFile; // e_cblp - Bytes on last page of file
public ushort PagesInFile; // e_cp - Pages in file
public ushort Relocations; // e_crlc - Relocations
public ushort SizeOfHeaderInParagraphs; // e_cparhdr - Size of header in paragraphs
public ushort MinimumExtraParagraphs; // e_minalloc - Minimum extra paragraphs needed
public ushort MaximumExtraParagraphs; // e_maxalloc - Maximum extra paragraphs needed
public ushort InitialSS; // e_ss - Initial (relative) SS value
public ushort InitialSP; // e_sp - Initial SP value
public ushort Checksum; // e_csum - Checksum
public ushort InitialIP; // e_ip - Initial IP value
public ushort InitialCS; // e_cs - Initial (relative) CS value
public ushort AddressOfRelocationTable; // e_lfarlc - File address of relocation table
public ushort OverlayNumber; // e_ovno - Overlay number
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public ushort[] ReservedWords1; // e_res - Reserved words
public ushort OEMIdentifier; // e_oemid - OEM identifier (for e_oeminfo)
public ushort OEMInformation; // e_oeminfo - OEM information; e_oemid specific
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public ushort[] ReservedWords2; // e_res2 - Reserved words
public int FileAddressOfNewExeHeader; // e_lfanew - File address of new exe header
}
/// <summary>
/// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_file_header
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct IMAGE_FILE_HEADER
{
/// <summary>
/// The architecture type of the computer.
/// An image file can only be run on the specified computer or a system that emulates the specified computer.
/// </summary>
public ushort Machine;
/// <summary>
/// The number of sections.
/// This indicates the size of the section table, which immediately follows the headers.
/// Note that the Windows loader limits the number of sections to 96.
/// </summary>
public ushort NumberOfSections;
/// <summary>
/// The low 32 bits of the time stamp of the image.
/// This represents the date and time the image was created by the linker.
/// The value is represented in the number of seconds elapsed since midnight (00:00:00), January 1, 1970, Universal Coordinated Time, according to the system clock.
/// </summary>
public uint TimeDateStamp;
public uint PointerToSymbolTable;
public uint NumberOfSymbols;
public ushort SizeOfOptionalHeader;
public ushort Characteristics;
}
/// <summary>
/// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_nt_headers32
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct IMAGE_NT_HEADERS32
{
public uint Signature; // DWORD Signature
public IMAGE_FILE_HEADER FileHeader; // IMAGE_FILE_HEADER FileHeader
public IMAGE_OPTIONAL_HEADER32 OptionalHeader; // IMAGE_OPTIONAL_HEADER32 OptionalHeader
}
/// <summary>
/// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_nt_headers64
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct IMAGE_NT_HEADERS64
{
public uint Signature; // DWORD Signature
public IMAGE_FILE_HEADER FileHeader; // IMAGE_FILE_HEADER FileHeader
public IMAGE_OPTIONAL_HEADER64 OptionalHeader; // IMAGE_OPTIONAL_HEADER32 OptionalHeader
}
/// <summary>
/// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header32
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct IMAGE_OPTIONAL_HEADER32
{
public ushort Magic;
public byte MajorLinkerVersion;
public byte MinorLinkerVersion;
public uint SizeOfCode;
public uint SizeOfInitializedData;
public uint SizeOfUninitializedData;
/// <summary>
/// A pointer to the entry point function, relative to the image base address.
/// For executable files, this is the starting address.
/// For device drivers, this is the address of the initialization function.
/// The entry point function is optional for DLLs.
/// When no entry point is present, this member is zero.
/// </summary>
public uint AddressOfEntryPoint;
/// <summary>
/// A pointer to the beginning of the code section, relative to the image base.
/// </summary>
public uint BaseOfCode;
/// <summary>
/// A pointer to the beginning of the data section, relative to the image base.
/// </summary>
public uint BaseOfData;
/// <summary>
/// The preferred address of the first byte of the image when it is loaded in memory.
/// This value is a multiple of 64K bytes.
/// The default value for DLLs is 0x10000000.
/// The default value for applications is 0x00400000, except on Windows CE where it is 0x00010000.
/// </summary>
public uint ImageBase;
public uint SectionAlignment;
public uint FileAlignment;
public ushort MajorOperatingSystemVersion;
public ushort MinorOperatingSystemVersion;
public ushort MajorImageVersion;
public ushort MinorImageVersion;
public ushort MajorSubsystemVersion;
public ushort MinorSubsystemVersion;
public uint Win32VersionValue;
/// <summary>
/// The size of the image, in bytes, including all headers. Must be a multiple of SectionAlignment.
/// </summary>
public uint SizeOfImage;
/// <summary>
/// The combined size of the following items, rounded to a multiple of the value specified in the FileAlignment member.
/// - e_lfanew member of IMAGE_DOS_HEADER
/// - 4 byte signature
/// - size of IMAGE_FILE_HEADER
/// - size of optional header
/// - size of all section headers
/// </summary>
public uint SizeOfHeaders;
public uint CheckSum;
public ushort Subsystem;
public ushort DllCharacteristics;
public uint SizeOfStackReserve;
public uint SizeOfStackCommit;
public uint SizeOfHeapReserve;
public uint SizeOfHeapCommit;
public uint LoaderFlags;
/// <summary>
/// The number of directory entries in the remainder of the optional header. Each entry describes a location and size.
/// </summary>
public uint NumberOfRvaAndSizes;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x10)]
public IMAGE_DATA_DIRECTORY[] DataDirectory;
}
/// <summary>
/// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header64
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct IMAGE_OPTIONAL_HEADER64
{
public ushort Magic;
public byte MajorLinkerVersion;
public byte MinorLinkerVersion;
public uint SizeOfCode;
public uint SizeOfInitializedData;
public uint SizeOfUninitializedData;
public uint AddressOfEntryPoint;
public uint BaseOfCode;
public ulong ImageBase;
public uint SectionAlignment;
public uint FileAlignment;
public ushort MajorOperatingSystemVersion;
public ushort MinorOperatingSystemVersion;
public ushort MajorImageVersion;
public ushort MinorImageVersion;
public ushort MajorSubsystemVersion;
public ushort MinorSubsystemVersion;
public uint Win32VersionValue;
public uint SizeOfImage;
public uint SizeOfHeaders;
public uint CheckSum;
public ushort Subsystem;
public ushort DllCharacteristics;
public ulong SizeOfStackReserve;
public ulong SizeOfStackCommit;
public ulong SizeOfHeapReserve;
public ulong SizeOfHeapCommit;
public uint LoaderFlags;
public uint NumberOfRvaAndSizes;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x10)]
public IMAGE_DATA_DIRECTORY[] DataDirectory;
}
/// <summary>
/// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_section_header
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct IMAGE_SECTION_HEADER
{
/// <summary>
/// An 8-byte, null-padded UTF-8 string.
/// There is no terminating null character if the string is exactly eight characters long.
/// For longer names, this member contains a forward slash (/) followed by an ASCII representation of a double number that is an offset into the string table.
/// Executable images do not use a string table and do not support section names longer than eight characters.
/// </summary>
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
public byte[] Name;
public UnionType Misc;
/// <summary>
/// The address of the first byte of the section when loaded into memory, relative to the image base.
/// For object files, this is the address of the first byte before relocation is applied.
/// </summary>
public uint VirtualAddress;
/// <summary>
/// The size of the initialized data on disk, in bytes.
/// This value must be a multiple of the FileAlignment member of the IMAGE_OPTIONAL_HEADER structure.
/// If this value is less than the VirtualSize member, the remainder of the section is filled with zeroes.
/// If the section contains only uninitialized data, the member is zero.
/// </summary>
public uint SizeOfRawData;
/// <summary>
/// A file pointer to the first page within the COFF file.
/// This value must be a multiple of the FileAlignment member of the IMAGE_OPTIONAL_HEADER structure.
/// If a section contains only uninitialized data, set this member is zero.
/// </summary>
public uint PointerToRawData;
public uint PointerToRelocations;
public uint PointerToLinenumbers;
public ushort NumberOfRelocations;
public ushort NumberOfLinenumbers;
public uint Characteristics;
[StructLayout(LayoutKind.Explicit)]
public struct UnionType
{
[FieldOffset(0)]
public uint PhysicalAddress;
/// <summary>
/// The total size of the section when loaded into memory, in bytes. If this value is greater than the SizeOfRawData member, the section is filled with zeroes.
/// This field is valid only for executable images and should be set to 0 for object files.
/// </summary>
[FieldOffset(0)]
public uint VirtualSize;
}
}

View File

@@ -0,0 +1,31 @@
using System.Runtime.InteropServices;
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
internal static class StreamExtensions
{
internal static void WriteStruct<T>(this Stream stream, T structData) where T : struct
{
var bytes = StructToBytes(structData);
stream.Write(bytes);
}
private static byte[] StructToBytes<T>(T structData) where T : struct
{
int size = Marshal.SizeOf(structData);
byte[] byteArray = new byte[size];
nint ptr = Marshal.AllocHGlobal(size);
try
{
Marshal.StructureToPtr(structData, ptr, false);
Marshal.Copy(ptr, byteArray, 0, size);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
return byteArray;
}
}

View File

@@ -0,0 +1,154 @@
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
internal class WasmWebcilUnwrapper : IAsyncDisposable
{
private readonly Stream _wasmStream;
private MemoryStream? _cachedStream;
public WasmWebcilUnwrapper(Stream wasmStream)
{
_wasmStream = wasmStream;
}
public async Task WriteUnwrappedAsync(Stream outputStream)
{
// Cache the stream content to MemoryStream for synchronous BinaryReader operations
if (_cachedStream == null)
{
_cachedStream = new MemoryStream();
await _wasmStream.CopyToAsync(_cachedStream);
_cachedStream.Position = 0; // Reset to beginning for validation
}
// Validate prefix from cached stream
ValidateWasmPrefix(_cachedStream);
// Skip prefix and read data section
var prefix = WasmWebcilWrapper.GetPrefix();
_cachedStream.Position = prefix.Length;
using var reader = new BinaryReader(_cachedStream, System.Text.Encoding.UTF8, leaveOpen: true);
var bytes = ReadDataSection(reader);
await outputStream.WriteAsync(bytes);
}
private void ValidateWasmPrefix(Stream stream)
{
var originalPosition = stream.Position;
try
{
// Create a byte array matching the length of the prefix.
var prefix = WasmWebcilWrapper.GetPrefix();
var buffer = new byte[prefix.Length];
stream.Position = 0;
int bytesRead = stream.Read(buffer, 0, buffer.Length);
if (bytesRead < buffer.Length)
{
throw new InvalidOperationException("Unable to read Wasm prefix.");
}
// Compare the read prefix with the expected one.
if (!buffer.SequenceEqual(prefix))
{
throw new InvalidOperationException("Invalid Wasm prefix.");
}
}
finally
{
stream.Position = originalPosition;
}
}
private static void SkipSection(BinaryReader reader)
{
var size = ULEB128Decode(reader);
reader.BaseStream.Seek(size, SeekOrigin.Current);
}
private static byte[] ReadDataSection(BinaryReader reader)
{
// Skip until we find the data section, which contains the Webcil payload.
byte[] buffer = new byte[1];
while (true)
{
// Read the Data section
var dataRead = reader.Read(buffer, 0, 1);
if (dataRead == 0)
{
throw new InvalidOperationException("Unable to read Data Section.");
}
// Check the Data section (ID = 11)
if (buffer[0] == 11)
{
break;
}
// Skip other sections by reading and ignoring their content.
SkipSection(reader);
}
// Read and ignore the size of the data section.
ULEB128Decode(reader);
// Read the number of segments.
int segmentsCount = (int)ULEB128Decode(reader);
int lastSegment = segmentsCount - 1;
for (int segmentIndex = 0; segmentIndex < segmentsCount; segmentIndex++)
{
// Ignore segmentType (1 = passive segment)
var segmentType = reader.Read(buffer, 0, 1);
if (segmentType != 1)
{
throw new InvalidOperationException($"Unexpected segment code for segment {segmentIndex}.");
}
// Read the segment size.
var segmentSize = ULEB128Decode(reader);
// The actual Webcil payload is expected to be in the last segment.
if (segmentIndex == lastSegment)
{
return reader.ReadBytes((int)segmentSize);
}
// Skip other segments.
reader.BaseStream.Seek(segmentSize, SeekOrigin.Current);
}
throw new Exception("Unable to read DataSection.");
}
/// <summary>
/// Decodes a variable-length quantity (VLQ) encoded as unsigned LEB128.
/// LEB128 (Little Endian Base 128) is used to encode integers in a variable number of bytes.
/// The method reads bytes from the provided binary reader and decodes them into an unsigned integer.
/// </summary>
/// <param name="reader">The binary reader from which to read the ULEB128 encoded data.</param>
/// <returns>The decoded unsigned integer from the ULEB128 encoded data.</returns>
private static uint ULEB128Decode(BinaryReader reader)
{
uint result = 0;
int shift = 0;
byte byteValue;
do
{
byteValue = reader.ReadByte();
uint byteAsUInt = byteValue & 0x7Fu;
result |= byteAsUInt << shift;
shift += 7;
} while ((byteValue & 0x80) != 0);
return result;
}
public async ValueTask DisposeAsync()
{
if (_cachedStream != null)
{
await _cachedStream.DisposeAsync();
}
await _wasmStream.DisposeAsync();
}
}

View File

@@ -0,0 +1,22 @@
using System.Reflection;
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
public class WasmWebcilWrapper
{
private static readonly FieldInfo FieldInfoPrefix = typeof(WebcilWasmWrapper).GetField("s_wasmWrapperPrefix", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.GetField)!;
public static byte[] GetPrefix()
{
#if NET7_0_OR_GREATER
return GetPrefixValue<ReadOnlyMemory<byte>>().ToArray();
#else
return GetPrefixValue<byte[]>();
#endif
}
private static T GetPrefixValue<T>()
{
return (T)FieldInfoPrefix.GetValue(null)!;
}
}

View File

@@ -0,0 +1,53 @@
using System.Runtime.InteropServices;
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
public class Webcil
{
/// <summary>
/// The header of a WebCIL file.
/// </summary>
///
/// <remarks>
/// The header is a subset of the PE, COFF and CLI headers that are needed by the mono runtime to load managed assemblies.
/// </remarks>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public unsafe struct WebcilHeader
{
public fixed byte id[4]; // 'W' 'b' 'I' 'L'
// 4 bytes
public ushort version_major; // 0
public ushort version_minor; // 0
// 8 bytes
public ushort coff_sections;
public ushort reserved0; // 0
// 12 bytes
public uint pe_cli_header_rva;
public uint pe_cli_header_size;
// 20 bytes
public uint pe_debug_rva;
public uint pe_debug_size;
// 28 bytes
}
/// <summary>
/// This is the Webcil analog of System.Reflection.PortableExecutable.SectionHeader, but with fewer fields
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct WebcilSectionHeader
{
public readonly int VirtualSize;
public readonly int VirtualAddress;
public readonly int SizeOfRawData;
public readonly int PointerToRawData;
public WebcilSectionHeader(int virtualSize, int virtualAddress, int sizeOfRawData, int pointerToRawData)
{
VirtualSize = virtualSize;
VirtualAddress = virtualAddress;
SizeOfRawData = sizeOfRawData;
PointerToRawData = pointerToRawData;
}
}
}

View File

@@ -0,0 +1,7 @@
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
internal static unsafe class WebcilConstants
{
public const int WC_VERSION_MAJOR = 0;
public const int WC_VERSION_MINOR = 0;
}

View File

@@ -0,0 +1,370 @@
using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Reflection.PortableExecutable;
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
/// <summary>
/// Reads a .NET assembly in a normal PE COFF file and writes it out as a Webcil file
/// </summary>
public class WebcilConverter
{
// Interesting stuff we've learned about the input PE file
public record PEFileInfo(
// The sections in the PE file
ImmutableArray<SectionHeader> SectionHeaders,
// The location of the debug directory entries
DirectoryEntry DebugTableDirectory,
// The file offset of the sections, following the section directory
FilePosition SectionStart,
// The debug directory entries
ImmutableArray<DebugDirectoryEntry> DebugDirectoryEntries
);
// Intersting stuff we know about the webcil file we're writing
public record WCFileInfo(
// The header of the webcil file
Webcil.WebcilHeader Header,
// The section directory of the webcil file
ImmutableArray<Webcil.WebcilSectionHeader> SectionHeaders,
// The file offset of the sections, following the section directory
FilePosition SectionStart
);
private readonly string _inputPath;
private readonly string _outputPath;
private string InputPath => _inputPath;
public bool WrapInWebAssembly { get; set; } = true;
private WebcilConverter(string inputPath, string outputPath)
{
_inputPath = inputPath;
_outputPath = outputPath;
}
public static WebcilConverter FromPortableExecutable(string inputPath, string outputPath)
=> new WebcilConverter(inputPath, outputPath);
public void ConvertToWebcil()
{
using var inputStream = File.Open(_inputPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
PEFileInfo peInfo;
WCFileInfo wcInfo;
using (var peReader = new PEReader(inputStream, PEStreamOptions.LeaveOpen))
{
GatherInfo(peReader, out wcInfo, out peInfo);
}
using var outputStream = File.Open(_outputPath, FileMode.Create, FileAccess.Write);
if (!WrapInWebAssembly)
{
WriteConversionTo(outputStream, inputStream, peInfo, wcInfo);
}
else
{
// if wrapping in WASM, write the webcil payload to memory because we need to discover the length
// webcil is about the same size as the PE file
using var memoryStream = new MemoryStream(checked((int)inputStream.Length));
WriteConversionTo(memoryStream, inputStream, peInfo, wcInfo);
memoryStream.Flush();
var wrapper = new WebcilWasmWrapper(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
wrapper.WriteWasmWrappedWebcil(outputStream);
}
}
public void WriteConversionTo(Stream outputStream, FileStream inputStream, PEFileInfo peInfo, WCFileInfo wcInfo)
{
WriteHeader(outputStream, wcInfo.Header);
WriteSectionHeaders(outputStream, wcInfo.SectionHeaders);
CopySections(outputStream, inputStream, peInfo.SectionHeaders);
if (wcInfo.Header.pe_debug_size != 0 && wcInfo.Header.pe_debug_rva != 0)
{
var wcDebugDirectoryEntries = FixupDebugDirectoryEntries(peInfo, wcInfo);
OverwriteDebugDirectoryEntries(outputStream, wcInfo, wcDebugDirectoryEntries);
}
}
public record struct FilePosition(int Position)
{
public static implicit operator FilePosition(int position) => new(position);
public static FilePosition operator +(FilePosition left, int right) => new(left.Position + right);
}
private static unsafe int SizeOfHeader()
{
return sizeof(Webcil.WebcilHeader);
}
public unsafe void GatherInfo(PEReader peReader, out WCFileInfo wcInfo, out PEFileInfo peInfo)
{
var headers = peReader.PEHeaders;
var peHeader = headers.PEHeader!;
var coffHeader = headers.CoffHeader!;
var sections = headers.SectionHeaders;
Webcil.WebcilHeader header;
header.id[0] = (byte)'W';
header.id[1] = (byte)'b';
header.id[2] = (byte)'I';
header.id[3] = (byte)'L';
header.version_major = WebcilConstants.WC_VERSION_MAJOR;
header.version_minor = WebcilConstants.WC_VERSION_MINOR;
header.coff_sections = (ushort)coffHeader.NumberOfSections;
header.reserved0 = 0;
header.pe_cli_header_rva = (uint)peHeader.CorHeaderTableDirectory.RelativeVirtualAddress;
header.pe_cli_header_size = (uint)peHeader.CorHeaderTableDirectory.Size;
header.pe_debug_rva = (uint)peHeader.DebugTableDirectory.RelativeVirtualAddress;
header.pe_debug_size = (uint)peHeader.DebugTableDirectory.Size;
// current logical position in the output file
FilePosition pos = SizeOfHeader();
// position of the current section in the output file
// initially it's after all the section headers
FilePosition curSectionPos = pos + sizeof(Webcil.WebcilSectionHeader) * coffHeader.NumberOfSections;
// The first WC section is immediately after the section directory
FilePosition firstWCSection = curSectionPos;
FilePosition firstPESection = 0;
ImmutableArray<Webcil.WebcilSectionHeader>.Builder headerBuilder = ImmutableArray.CreateBuilder<Webcil.WebcilSectionHeader>(coffHeader.NumberOfSections);
foreach (var sectionHeader in sections)
{
// The first section is the one with the lowest file offset
if (firstPESection.Position == 0)
{
firstPESection = sectionHeader.PointerToRawData;
}
else
{
firstPESection = Math.Min(firstPESection.Position, sectionHeader.PointerToRawData);
}
var newHeader = new Webcil.WebcilSectionHeader
(
virtualSize: sectionHeader.VirtualSize,
virtualAddress: sectionHeader.VirtualAddress,
sizeOfRawData: sectionHeader.SizeOfRawData,
pointerToRawData: curSectionPos.Position
);
pos += sizeof(Webcil.WebcilSectionHeader);
curSectionPos += sectionHeader.SizeOfRawData;
headerBuilder.Add(newHeader);
}
ImmutableArray<DebugDirectoryEntry> debugDirectoryEntries = peReader.ReadDebugDirectory();
peInfo = new PEFileInfo(SectionHeaders: sections,
DebugTableDirectory: peHeader.DebugTableDirectory,
SectionStart: firstPESection,
DebugDirectoryEntries: debugDirectoryEntries);
wcInfo = new WCFileInfo(Header: header,
SectionHeaders: headerBuilder.MoveToImmutable(),
SectionStart: firstWCSection);
}
private static void WriteHeader(Stream s, Webcil.WebcilHeader webcilHeader)
{
if (!BitConverter.IsLittleEndian)
{
webcilHeader.version_major = BinaryPrimitives.ReverseEndianness(webcilHeader.version_major);
webcilHeader.version_minor = BinaryPrimitives.ReverseEndianness(webcilHeader.version_minor);
webcilHeader.coff_sections = BinaryPrimitives.ReverseEndianness(webcilHeader.coff_sections);
webcilHeader.pe_cli_header_rva = BinaryPrimitives.ReverseEndianness(webcilHeader.pe_cli_header_rva);
webcilHeader.pe_cli_header_size = BinaryPrimitives.ReverseEndianness(webcilHeader.pe_cli_header_size);
webcilHeader.pe_debug_rva = BinaryPrimitives.ReverseEndianness(webcilHeader.pe_debug_rva);
webcilHeader.pe_debug_size = BinaryPrimitives.ReverseEndianness(webcilHeader.pe_debug_size);
}
WriteStructure(s, webcilHeader);
}
private static void WriteSectionHeaders(Stream s, ImmutableArray<Webcil.WebcilSectionHeader> sectionsHeaders)
{
foreach (var sectionHeader in sectionsHeaders)
{
WriteSectionHeader(s, sectionHeader);
}
}
private static void WriteSectionHeader(Stream s, Webcil.WebcilSectionHeader sectionHeader)
{
if (!BitConverter.IsLittleEndian)
{
sectionHeader = new Webcil.WebcilSectionHeader
(
virtualSize: BinaryPrimitives.ReverseEndianness(sectionHeader.VirtualSize),
virtualAddress: BinaryPrimitives.ReverseEndianness(sectionHeader.VirtualAddress),
sizeOfRawData: BinaryPrimitives.ReverseEndianness(sectionHeader.SizeOfRawData),
pointerToRawData: BinaryPrimitives.ReverseEndianness(sectionHeader.PointerToRawData)
);
}
WriteStructure(s, sectionHeader);
}
#if NETCOREAPP2_1_OR_GREATER
private static void WriteStructure<T>(Stream s, T structure)
where T : unmanaged
{
unsafe
{
byte* p = (byte*)&structure;
s.Write(new ReadOnlySpan<byte>(p, sizeof(T)));
}
}
#else
private static void WriteStructure<T>(Stream s, T structure)
where T : unmanaged
{
int size = Marshal.SizeOf<T>();
byte[] buffer = new byte[size];
IntPtr ptr = IntPtr.Zero;
try
{
ptr = Marshal.AllocHGlobal(size);
Marshal.StructureToPtr(structure, ptr, false);
Marshal.Copy(ptr, buffer, 0, size);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
s.Write(buffer, 0, size);
}
#endif
private static void CopySections(Stream outStream, FileStream inputStream, ImmutableArray<SectionHeader> peSections)
{
// endianness: ok, we're just copying from one stream to another
foreach (var peHeader in peSections)
{
var buffer = new byte[peHeader.SizeOfRawData];
inputStream.Seek(peHeader.PointerToRawData, SeekOrigin.Begin);
ReadExactly(inputStream, buffer);
outStream.Write(buffer, 0, buffer.Length);
}
}
#if NETCOREAPP2_1_OR_GREATER && !NET6_0
private static void ReadExactly(FileStream s, Span<byte> buffer)
{
s.ReadExactly(buffer);
}
#else
private static void ReadExactly(FileStream s, byte[] buffer)
{
int offset = 0;
while (offset < buffer.Length)
{
int read = s.Read(buffer, offset, buffer.Length - offset);
if (read == 0)
throw new EndOfStreamException();
offset += read;
}
}
#endif
private static FilePosition GetPositionOfRelativeVirtualAddress(ImmutableArray<Webcil.WebcilSectionHeader> wcSections, uint relativeVirtualAddress)
{
foreach (var section in wcSections)
{
if (relativeVirtualAddress >= section.VirtualAddress && relativeVirtualAddress < section.VirtualAddress + section.VirtualSize)
{
FilePosition pos = section.PointerToRawData + ((int)relativeVirtualAddress - section.VirtualAddress);
return pos;
}
}
throw new InvalidOperationException("relative virtual address not in any section");
}
// Given a physical file offset, return the section and the offset within the section.
private (Webcil.WebcilSectionHeader section, int offset) GetSectionFromFileOffset(ImmutableArray<Webcil.WebcilSectionHeader> peSections, FilePosition fileOffset)
{
foreach (var section in peSections)
{
if (fileOffset.Position >= section.PointerToRawData && fileOffset.Position < section.PointerToRawData + section.SizeOfRawData)
{
return (section, fileOffset.Position - section.PointerToRawData);
}
}
throw new InvalidOperationException($"file offset not in any section (Webcil) for {InputPath}");
}
private void GetSectionFromFileOffset(ImmutableArray<SectionHeader> sections, FilePosition fileOffset)
{
foreach (var section in sections)
{
if (fileOffset.Position >= section.PointerToRawData && fileOffset.Position < section.PointerToRawData + section.SizeOfRawData)
{
return;
}
}
throw new InvalidOperationException($"file offset {fileOffset.Position} not in any section (PE) for {InputPath}");
}
// Make a new set of debug directory entries that
// have their data pointers adjusted to be relative to the start of the webcil file.
// This is necessary because the debug directory entires in the PE file are relative to the start of the PE file,
// and a PE header is bigger than a webcil header.
private ImmutableArray<DebugDirectoryEntry> FixupDebugDirectoryEntries(PEFileInfo peInfo, WCFileInfo wcInfo)
{
int dataPointerAdjustment = peInfo.SectionStart.Position - wcInfo.SectionStart.Position;
ImmutableArray<DebugDirectoryEntry> entries = peInfo.DebugDirectoryEntries;
ImmutableArray<DebugDirectoryEntry>.Builder newEntries = ImmutableArray.CreateBuilder<DebugDirectoryEntry>(entries.Length);
foreach (var entry in entries)
{
DebugDirectoryEntry newEntry;
if (entry.Type == DebugDirectoryEntryType.Reproducible || entry.DataPointer == 0 || entry.DataSize == 0)
{
// this entry doesn't have an associated data pointer, so just copy it
newEntry = entry;
}
else
{
// the "DataPointer" field is a file offset in the PE file, adjust the entry wit the corresponding offset in the Webcil file
var newDataPointer = entry.DataPointer - dataPointerAdjustment;
newEntry = new DebugDirectoryEntry(entry.Stamp, entry.MajorVersion, entry.MinorVersion, entry.Type, entry.DataSize, entry.DataRelativeVirtualAddress, newDataPointer);
GetSectionFromFileOffset(peInfo.SectionHeaders, entry.DataPointer);
// validate that the new entry is in some section
GetSectionFromFileOffset(wcInfo.SectionHeaders, newDataPointer);
}
newEntries.Add(newEntry);
}
return newEntries.MoveToImmutable();
}
private static void OverwriteDebugDirectoryEntries(Stream s, WCFileInfo wcInfo, ImmutableArray<DebugDirectoryEntry> entries)
{
FilePosition debugDirectoryPos = GetPositionOfRelativeVirtualAddress(wcInfo.SectionHeaders, wcInfo.Header.pe_debug_rva);
using var writer = new BinaryWriter(s, System.Text.Encoding.UTF8, leaveOpen: true);
writer.Seek(debugDirectoryPos.Position, SeekOrigin.Begin);
foreach (var entry in entries)
{
WriteDebugDirectoryEntry(writer, entry);
}
// TODO check that we overwrite with the same size as the original
// restore the stream position
writer.Seek(0, SeekOrigin.End);
}
private static void WriteDebugDirectoryEntry(BinaryWriter writer, DebugDirectoryEntry entry)
{
writer.Write((uint)0); // Characteristics
writer.Write(entry.Stamp);
writer.Write(entry.MajorVersion);
writer.Write(entry.MinorVersion);
writer.Write((uint)entry.Type);
writer.Write(entry.DataSize);
writer.Write(entry.DataRelativeVirtualAddress);
writer.Write(entry.DataPointer);
}
}

View File

@@ -0,0 +1,379 @@
using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using Microsoft.CodeAnalysis;
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
internal static class WebcilConverterUtil
{
private static readonly byte[] SectionHeaderText = { 0x2E, 0x74, 0x65, 0x78, 0x74, 0x00, 0x00, 0x00 }; // .text
private static readonly byte[] SectionHeaderRsRc = { 0x2E, 0x72, 0x73, 0x72, 0x63, 0x00, 0x00, 0x00 }; // .rsrc
private static readonly byte[] SectionHeaderReloc = { 0x2E, 0x72, 0x65, 0x6C, 0x6F, 0x63, 0x00, 0x00 }; // .reloc
private static readonly byte[] MSDOS =
{
0x0E, 0x1F, 0xBA, 0x0E, 0x00, 0xB4, 0x09, 0xCD, 0x21, 0xB8, 0x01, 0x4C, 0xCD, 0x21, 0x54, 0x68,
0x69, 0x73, 0x20, 0x70, 0x72, 0x6F, 0x67, 0x72, 0x61, 0x6D, 0x20, 0x63, 0x61, 0x6E, 0x6E, 0x6F,
0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x75, 0x6E, 0x20, 0x69, 0x6E, 0x20, 0x44, 0x4F, 0x53, 0x20,
0x6D, 0x6F, 0x64, 0x65, 0x2E, 0x0D, 0x0D, 0x0A, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
private static readonly ushort[] DOSReservedWords1 = { 0, 0, 0, 0 };
private static readonly ushort[] DOSReservedWords2 = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
private static readonly DateTime Epoch = new(1970, 1, 1);
private static readonly int SizeofDOSHeader = Marshal.SizeOf<IMAGE_DOS_HEADER>(); // 64
private static readonly int SizeofFileHeader = Marshal.SizeOf<IMAGE_FILE_HEADER>();
private static readonly int SizeofMSDOS = MSDOS.Length; // 64
private static readonly int SizeofNTHeaders = Marshal.SizeOf<IMAGE_NT_HEADERS32>(); // 248
private static readonly int SizeofOptionalHeader = Marshal.SizeOf<IMAGE_OPTIONAL_HEADER32>();
private static readonly int SizeofSectionHeader = Marshal.SizeOf<IMAGE_SECTION_HEADER>(); // 40
private const uint FileAlignment = 0x0200;
private const uint SectionAlignment = 0x2000;
/// <summary>
/// Convert a Portable Executable file into a Webcil file.
/// </summary>
/// <param name="inputPath">The input path for the PE file.</param>
/// <param name="outputPath">The output path for the Webcil file.</param>
/// <param name="wrapInWebAssembly">The Webcil should be wrapped in Wasm [default value is <c>true</c>].</param>
public static void ConvertToWebcil(string inputPath, string outputPath, bool wrapInWebAssembly = true)
{
var webcilConverter = WebcilConverter.FromPortableExecutable(inputPath, outputPath);
webcilConverter.WrapInWebAssembly = wrapInWebAssembly;
webcilConverter.ConvertToWebcil();
}
/// <summary>
/// Convert a Webcil stream into a Portable Executable which can be used to create a valid <see cref="MetadataReference"/>.
/// </summary>
/// <param name="inputStream">The input sStream.</param>
/// <param name="wrappedInWebAssembly">The Webcil is wrapped in Wasm [default value is <c>true</c>].</param>
/// <returns>A byte[] Portable Executable</returns>
public static async Task<byte[]> ConvertFromWebcilAsync(Stream inputStream, bool wrappedInWebAssembly = true)
{
Stream webcilStream;
if (wrappedInWebAssembly)
{
await using var unwrapper = new WasmWebcilUnwrapper(inputStream);
webcilStream = new MemoryStream();
await unwrapper.WriteUnwrappedAsync(webcilStream);
webcilStream.Flush();
webcilStream.Seek(0, SeekOrigin.Begin);
}
else
{
webcilStream = inputStream;
}
// These are Webcil variables
var webcilHeader = ReadHeader(webcilStream);
var webcilSectionHeaders = ReadSectionHeaders(webcilStream, webcilHeader.coff_sections);
var webcilSectionHeadersCount = webcilSectionHeaders.Length;
var webcilSectionHeadersSizeOfRawData = (uint)webcilSectionHeaders.Sum(x => x.SizeOfRawData);
// These are PE (Portable Executable) variables
int sectionStart = SizeofDOSHeader + SizeofMSDOS + SizeofNTHeaders + webcilSectionHeadersCount * SizeofSectionHeader; // 496
int sectionStartRounded = sectionStart.RoundToNearest();
var extraBytesAfterSections = new byte[sectionStartRounded - sectionStart];
var pointerToRawDataFirstSectionHeader = webcilSectionHeaders[0].PointerToRawData;
var pointerToRawDataOffsetBetweenWebcilAndPE = sectionStartRounded - pointerToRawDataFirstSectionHeader;
using var peStream = new MemoryStream();
var DOSHeader = new IMAGE_DOS_HEADER
{
MagicNumber = 0x5A4D,
BytesOnLastPageOfFile = 0x90,
PagesInFile = 3,
Relocations = 0,
SizeOfHeaderInParagraphs = 4,
MinimumExtraParagraphs = 0,
MaximumExtraParagraphs = 0xFFFF,
InitialSS = 0,
InitialSP = 0xB8,
Checksum = 0,
InitialIP = 0,
InitialCS = 0,
AddressOfRelocationTable = 0x40,
OverlayNumber = 0,
ReservedWords1 = DOSReservedWords1,
OEMIdentifier = 0,
OEMInformation = 0,
ReservedWords2 = DOSReservedWords2,
FileAddressOfNewExeHeader = 0x80
};
peStream.WriteStruct(DOSHeader);
peStream.Write(MSDOS);
var IMAGE_NT_HEADERS32 = new IMAGE_NT_HEADERS32
{
Signature = 0x4550, // 'PE'
FileHeader = new IMAGE_FILE_HEADER
{
Machine = Constants.IMAGE_FILE_MACHINE_I386,
NumberOfSections = 3,
TimeDateStamp = GetImageTimestamp(),
PointerToSymbolTable = 0,
NumberOfSymbols = 0,
SizeOfOptionalHeader = 0x00E0,
Characteristics = 0x0022
},
OptionalHeader = new IMAGE_OPTIONAL_HEADER32
{
Magic = 0x010B, // Signature/Magic - Represents PE32 for 32-bit (0x10b) and PE32+ for 64-bit (0x20B)
MajorLinkerVersion = 0x30,
MinorLinkerVersion = 0,
SizeOfCode = (uint)webcilSectionHeaders[0].SizeOfRawData,
SizeOfInitializedData = (uint)(webcilSectionHeaders[1].SizeOfRawData + webcilSectionHeaders[2].SizeOfRawData),
SizeOfUninitializedData = 0,
AddressOfEntryPoint = 0, // This can be set to 0
BaseOfCode = 0x2000,
BaseOfData = 0xA000,
ImageBase = 0x400000, // The default value for applications is 0x00400000
SectionAlignment = SectionAlignment,
FileAlignment = FileAlignment,
MajorOperatingSystemVersion = 4,
MinorOperatingSystemVersion = 0,
MajorImageVersion = 0,
MinorImageVersion = 0,
MajorSubsystemVersion = 4,
MinorSubsystemVersion = 0,
Win32VersionValue = 0,
SizeOfImage = webcilSectionHeadersSizeOfRawData.RoundToNearest(SectionAlignment),
SizeOfHeaders = GetSizeOfHeaders(DOSHeader, webcilSectionHeadersCount),
CheckSum = 0,
Subsystem = 3, // IMAGE_SUBSYSTEM_WINDOWS_CUI
DllCharacteristics = 0x8560,
SizeOfStackReserve = 0x100000,
SizeOfStackCommit = 0x1000,
SizeOfHeapReserve = 0x100000,
SizeOfHeapCommit = 0x1000,
LoaderFlags = 0,
NumberOfRvaAndSizes = 0x10,
DataDirectory = new IMAGE_DATA_DIRECTORY[]
{
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_EXPORT
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_IMPORT (can be 0)
new() { Size = (uint) webcilSectionHeaders[1].VirtualSize, VirtualAddress = (uint) webcilSectionHeaders[1].VirtualAddress }, // IMAGE_DIRECTORY_ENTRY_RESOURCE
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_EXCEPTION
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_SECURITY
new() { Size = (uint) webcilSectionHeaders[2].VirtualSize, VirtualAddress = (uint) webcilSectionHeaders[2].VirtualAddress }, // IMAGE_DIRECTORY_ENTRY_BASERELOC
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_DEBUG (can be 0)
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_ARCHITECTURE
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_GLOBALPTR
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_TLS
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT
new() { Size = 0x0008, VirtualAddress = (uint) webcilSectionHeaders[0].VirtualAddress }, // IMAGE_DIRECTORY_ENTRY_IAT
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT
new() { Size = 0x0048, VirtualAddress = (uint) webcilSectionHeaders[0].VirtualAddress + 8 }, // TODO ??? IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR
new() { Size = 0x0000, VirtualAddress = 0x0000 } // ?
}
}
};
peStream.WriteStruct(IMAGE_NT_HEADERS32);
var textSectionHeader = new IMAGE_SECTION_HEADER
{
Name = SectionHeaderText,
Misc = new IMAGE_SECTION_HEADER.UnionType { VirtualSize = (uint)webcilSectionHeaders[0].VirtualSize },
VirtualAddress = (uint)webcilSectionHeaders[0].VirtualAddress,
SizeOfRawData = (uint)webcilSectionHeaders[0].SizeOfRawData,
PointerToRawData = webcilSectionHeaders[0].GetCorrectedPointerToRawData(pointerToRawDataOffsetBetweenWebcilAndPE),
Characteristics = 0x60000020
};
peStream.WriteStruct(textSectionHeader);
var rsrcSectionHeader = new IMAGE_SECTION_HEADER
{
Name = SectionHeaderRsRc,
Misc = new IMAGE_SECTION_HEADER.UnionType { VirtualSize = (uint)webcilSectionHeaders[1].VirtualSize },
VirtualAddress = (uint)webcilSectionHeaders[1].VirtualAddress,
SizeOfRawData = (uint)webcilSectionHeaders[1].SizeOfRawData,
PointerToRawData = webcilSectionHeaders[1].GetCorrectedPointerToRawData(pointerToRawDataOffsetBetweenWebcilAndPE),
Characteristics = 0x40000040
};
peStream.WriteStruct(rsrcSectionHeader);
var relocSectionHeader = new IMAGE_SECTION_HEADER
{
Name = SectionHeaderReloc,
Misc = new IMAGE_SECTION_HEADER.UnionType { VirtualSize = (uint)webcilSectionHeaders[2].VirtualSize },
VirtualAddress = (uint)webcilSectionHeaders[2].VirtualAddress,
SizeOfRawData = (uint)webcilSectionHeaders[2].SizeOfRawData,
PointerToRawData = webcilSectionHeaders[2].GetCorrectedPointerToRawData(pointerToRawDataOffsetBetweenWebcilAndPE),
Characteristics = 0x42000040
};
peStream.WriteStruct(relocSectionHeader);
if (extraBytesAfterSections.Length > 0)
{
peStream.Write(extraBytesAfterSections);
}
// Just copy all data
foreach (var webcilSectionHeader in webcilSectionHeaders)
{
var buffer = new byte[webcilSectionHeader.SizeOfRawData];
webcilStream.Seek(webcilSectionHeader.PointerToRawData, SeekOrigin.Begin);
ReadExactly(webcilStream, buffer);
peStream.Write(buffer, 0, buffer.Length);
}
peStream.Flush();
peStream.Seek(0, SeekOrigin.Begin);
return peStream.ToArray();
}
private static Webcil.WebcilHeader ReadHeader(Stream webcilStream)
{
var webcilHeader = ReadStructure<Webcil.WebcilHeader>(webcilStream);
if (!BitConverter.IsLittleEndian)
{
webcilHeader.version_major = BinaryPrimitives.ReverseEndianness(webcilHeader.version_major);
webcilHeader.version_minor = BinaryPrimitives.ReverseEndianness(webcilHeader.version_minor);
webcilHeader.coff_sections = BinaryPrimitives.ReverseEndianness(webcilHeader.coff_sections);
webcilHeader.pe_cli_header_rva = BinaryPrimitives.ReverseEndianness(webcilHeader.pe_cli_header_rva);
webcilHeader.pe_cli_header_size = BinaryPrimitives.ReverseEndianness(webcilHeader.pe_cli_header_size);
webcilHeader.pe_debug_rva = BinaryPrimitives.ReverseEndianness(webcilHeader.pe_debug_rva);
webcilHeader.pe_debug_size = BinaryPrimitives.ReverseEndianness(webcilHeader.pe_debug_size);
}
return webcilHeader;
}
private static ImmutableArray<Webcil.WebcilSectionHeader> ReadSectionHeaders(Stream webcilStream, int sectionsHeaders)
{
var result = new List<Webcil.WebcilSectionHeader>();
for (int i = 0; i < sectionsHeaders; i++)
{
result.Add(ReadSectionHeader(webcilStream));
}
return ImmutableArray.Create(result.ToArray());
}
private static Webcil.WebcilSectionHeader ReadSectionHeader(Stream webcilStream)
{
var sectionHeader = ReadStructure<Webcil.WebcilSectionHeader>(webcilStream);
if (!BitConverter.IsLittleEndian)
{
sectionHeader = new Webcil.WebcilSectionHeader
(
virtualSize: BinaryPrimitives.ReverseEndianness(sectionHeader.VirtualSize),
virtualAddress: BinaryPrimitives.ReverseEndianness(sectionHeader.VirtualAddress),
sizeOfRawData: BinaryPrimitives.ReverseEndianness(sectionHeader.SizeOfRawData),
pointerToRawData: BinaryPrimitives.ReverseEndianness(sectionHeader.PointerToRawData)
);
}
return sectionHeader;
}
private static uint GetSizeOfHeaders(IMAGE_DOS_HEADER IMAGE_DOS_HEADER, int numSectionHeaders)
{
var soh = IMAGE_DOS_HEADER.FileAddressOfNewExeHeader + // e_lfanew member of IMAGE_DOS_HEADER
sizeof(uint) + // 4 byte signature
SizeofFileHeader +
SizeofOptionalHeader + // size of optional header
numSectionHeaders * SizeofSectionHeader // size of all section headers
;
return (uint)soh.RoundToNearest();
}
/// <summary>
/// The low 32 bits of the time stamp of the image.
/// This represents the date and time the image was created by the linker.
/// The value is represented in the number of seconds elapsed since midnight (00:00:00), January 1, 1970, Universal Coordinated Time, according to the system clock.
/// </summary>
private static uint GetImageTimestamp()
{
// Calculate the total seconds since Unix epoch
var totalSeconds = (DateTime.UtcNow - Epoch).Ticks / TimeSpan.TicksPerSecond;
// Convert to uint (low 32 bits)
return (uint)totalSeconds;
}
internal static int RoundToNearest(this int number, int nearest = 512)
{
int remainder = number % nearest;
int halfNearest = nearest / 2;
return remainder >= halfNearest ? number + nearest - remainder : number - remainder;
}
internal static uint RoundToNearest(this uint number, uint nearest = 512)
{
uint remainder = number % nearest;
uint halfNearest = nearest / 2;
return remainder >= halfNearest ? number + nearest - remainder : number - remainder;
}
#if NETCOREAPP2_1_OR_GREATER
private static T ReadStructure<T>(Stream s) where T : unmanaged
{
T structure = default;
unsafe
{
byte* p = (byte*)&structure;
Span<byte> buffer = new Span<byte>(p, sizeof(T));
int read = s.Read(buffer);
if (read != sizeof(T))
{
throw new InvalidOperationException("Couldn't read the full structure from the stream.");
}
}
return structure;
}
#else
private static T ReadStructure<T>(Stream s) where T : unmanaged
{
int size = Marshal.SizeOf<T>();
byte[] buffer = new byte[size];
s.Read(buffer, 0, size);
IntPtr ptr = IntPtr.Zero;
try
{
ptr = Marshal.AllocHGlobal(size);
Marshal.Copy(buffer, 0, ptr, size);
return Marshal.PtrToStructure<T>(ptr);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
}
#endif
#if NETCOREAPP2_1_OR_GREATER && !NET6_0
private static void ReadExactly(Stream s, Span<byte> buffer)
{
s.ReadExactly(buffer);
}
#else
private static void ReadExactly(Stream s, byte[] buffer)
{
int offset = 0;
while (offset < buffer.Length)
{
int read = s.Read(buffer, offset, buffer.Length - offset);
if (read == 0)
{
throw new EndOfStreamException();
}
offset += read;
}
}
#endif
}

View File

@@ -0,0 +1,9 @@
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
internal static class WebcilSectionHeaderExtensions
{
internal static uint GetCorrectedPointerToRawData(this Webcil.WebcilSectionHeader webcilSectionHeader, int offset)
{
return (uint) (webcilSectionHeader.PointerToRawData + offset);
}
}

View File

@@ -0,0 +1,222 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
//
// Emits a simple WebAssembly wrapper module around a given webcil payload.
//
// The entire wasm module is going to be unchanging, except for the data section which has 2 passive
// segments. segment 0 is 4 bytes and contains the length of the webcil payload. segment 1 is of a
// variable size and contains the webcil payload.
//
// The unchanging parts are stored as a "prefix" and "suffix" which contain the bytes for the following
// WAT module, split into the parts that come before the data section, and the bytes that come after:
//
// (module
// (data "\0\00\00\00") ;; data segment 0: payload size as a 4 byte LE uint32
// (data "webcil Payload\cc") ;; data segment 1: webcil payload
// (memory (import "webcil" "memory") 1)
// (global (export "webcilVersion") i32 (i32.const 0))
// (func (export "getWebcilSize") (param $destPtr i32) (result)
// local.get $destPtr
// i32.const 0
// i32.const 4
// memory.init 0)
// (func (export "getWebcilPayload") (param $d i32) (param $n i32) (result)
// local.get $d
// i32.const 0
// local.get $n
// memory.init 1))
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
public class WebcilWasmWrapper
{
private readonly Stream _webcilPayloadStream;
private readonly uint _webcilPayloadSize;
public WebcilWasmWrapper(Stream webcilPayloadStream)
{
_webcilPayloadStream = webcilPayloadStream;
long len = webcilPayloadStream.Length;
if (len > (long)uint.MaxValue)
throw new InvalidOperationException("webcil payload too large");
_webcilPayloadSize = (uint)len;
}
public void WriteWasmWrappedWebcil(Stream outputStream)
{
WriteWasmHeader(outputStream);
using (var writer = new BinaryWriter(outputStream, System.Text.Encoding.UTF8, leaveOpen: true))
{
WriteDataSection(writer);
}
WriteWasmSuffix(outputStream);
}
//
// Everything from the above wat module before the data section
//
// extracted by wasm-reader -s wrapper.wasm
private static
#if NET7_0_OR_GREATER
ReadOnlyMemory<byte>
#else
byte[]
#endif
s_wasmWrapperPrefix = new byte[] {
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x0a, 0x02, 0x60, 0x01, 0x7f, 0x00, 0x60, 0x02, 0x7f, 0x7f, 0x00, 0x02, 0x12, 0x01, 0x06, 0x77, 0x65, 0x62, 0x63, 0x69, 0x6c, 0x06, 0x6d,
0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x01, 0x03, 0x03, 0x02, 0x00, 0x01, 0x06, 0x0b, 0x02, 0x7f, 0x00, 0x41, 0x00, 0x0b, 0x7f, 0x00, 0x41, 0x00, 0x0b, 0x07, 0x41, 0x04, 0x0d, 0x77, 0x65,
0x62, 0x63, 0x69, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x03, 0x00, 0x0a, 0x77, 0x65, 0x62, 0x63, 0x69, 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x03, 0x01, 0x0d, 0x67, 0x65, 0x74, 0x57, 0x65,
0x62, 0x63, 0x69, 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x00, 0x00, 0x10, 0x67, 0x65, 0x74, 0x57, 0x65, 0x62, 0x63, 0x69, 0x6c, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x00, 0x01, 0x0c, 0x01, 0x02,
0x0a, 0x1b, 0x02, 0x0c, 0x00, 0x20, 0x00, 0x41, 0x00, 0x41, 0x04, 0xfc, 0x08, 0x00, 0x00, 0x0b, 0x0c, 0x00, 0x20, 0x00, 0x41, 0x00, 0x20, 0x01, 0xfc, 0x08, 0x01, 0x00, 0x0b,
};
//
// Everything from the above wat module after the data section
//
// extracted by wasm-reader -s wrapper.wasm
private static
#if NET7_0_OR_GREATER
ReadOnlyMemory<byte>
#else
byte[]
#endif
s_wasmWrapperSuffix = new byte[] {
0x00, 0x1b, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x02, 0x14, 0x02, 0x00, 0x01, 0x00, 0x07, 0x64, 0x65, 0x73, 0x74, 0x50, 0x74, 0x72, 0x01, 0x02, 0x00, 0x01, 0x64, 0x01, 0x01, 0x6e,
};
private static void WriteWasmHeader(Stream outputStream)
{
#if NET7_0_OR_GREATER
outputStream.Write(s_wasmWrapperPrefix.Span);
#else
outputStream.Write(s_wasmWrapperPrefix, 0, s_wasmWrapperPrefix.Length);
#endif
}
private static void WriteWasmSuffix(Stream outputStream)
{
#if NET7_0_OR_GREATER
outputStream.Write(s_wasmWrapperSuffix.Span);
#else
outputStream.Write(s_wasmWrapperSuffix, 0, s_wasmWrapperSuffix.Length);
#endif
}
// 1 byte to encode "passive" data segment
private const uint SegmentCodeSize = 1;
// Align the payload start to a 4-byte boundary within the wrapper. If the runtime reads the
// payload directly, instead of by instantiatng the wasm module, we don't want the WebAssembly
// prefix to push some of the values inside the image to odd byte offsets as the runtime assumes
// the image will be aligned.
//
// There are requirements in ECMA-335 (Section II.25.4) that fat method headers and method data
// sections be 4-byte aligned.
private const uint WebcilPayloadInternalAlignment = 4;
private void WriteDataSection(BinaryWriter writer)
{
uint dataSectionSize = 0;
// uleb128 encoding of number of segments
dataSectionSize += 1; // there's always 2 segments which encodes to 1 byte
// compute the segment 0 size:
// segment 0 has 1 byte segment code, 1 byte of size and at least 4 bytes of payload
uint segment0MinimumSize = SegmentCodeSize + 1 + 4;
dataSectionSize += segment0MinimumSize;
// encode webcil size as a uleb128
byte[] ulebWebcilPayloadSize = ULEB128Encode(_webcilPayloadSize);
// compute the segment 1 size:
// segment 1 has 1 byte segment code, a uleb128 encoding of the webcilPayloadSize, and the payload
// don't count the size of the payload yet
checked
{
dataSectionSize += SegmentCodeSize + (uint)ulebWebcilPayloadSize.Length;
}
// at this point the data section size includes everything except the data section code, the data section size and the webcil payload itself
// and any extra padding that we may want to add to segment 0.
// So we can compute the offset of the payload within the wasm module.
byte[] putativeULEBDataSectionSize = ULEB128Encode(dataSectionSize + _webcilPayloadSize);
uint payloadOffset = (uint)s_wasmWrapperPrefix.Length + 1 + (uint)putativeULEBDataSectionSize.Length + dataSectionSize ;
uint paddingSize = PadTo(payloadOffset, WebcilPayloadInternalAlignment);
if (paddingSize > 0)
{
checked
{
dataSectionSize += paddingSize;
}
}
checked
{
dataSectionSize += _webcilPayloadSize;
}
byte[] ulebSectionSize = ULEB128Encode(dataSectionSize);
if (putativeULEBDataSectionSize.Length != ulebSectionSize.Length)
throw new InvalidOperationException ("adding padding would cause data section's encoded length to chane"); // TODO: fixme: there's upto one extra byte to encode the section length - take away a padding byte.
writer.Write((byte)11); // section Data
writer.Write(ulebSectionSize, 0, ulebSectionSize.Length);
writer.Write((byte)2); // number of segments
// write segment 0
writer.Write((byte)1); // passive segment
if (paddingSize + 4 > 127) {
throw new InvalidOperationException ("padding would cause segment 0 to need a multi-byte ULEB128 size encoding");
}
writer.Write((byte)(4 + paddingSize)); // segment size: 4 plus any padding
writer.Write((uint)_webcilPayloadSize); // payload is an unsigned 32 bit number
for (int i = 0; i < paddingSize; i++)
writer.Write((byte)0);
// write segment 1
writer.Write((byte)1); // passive segment
writer.Write(ulebWebcilPayloadSize, 0, ulebWebcilPayloadSize.Length); // segment size: _webcilPayloadSize
if (writer.BaseStream.Position % WebcilPayloadInternalAlignment != 0) {
throw new Exception ($"predited offset {payloadOffset}, actual position {writer.BaseStream.Position}");
}
_webcilPayloadStream.CopyTo(writer.BaseStream); // payload is the entire webcil content
}
private static byte[] ULEB128Encode(uint value)
{
uint n = value;
int len = 0;
do
{
n >>= 7;
len++;
} while (n != 0);
byte[] arr = new byte[len];
int i = 0;
n = value;
do
{
byte b = (byte)(n & 0x7f);
n >>= 7;
if (n != 0)
b |= 0x80;
arr[i++] = b;
} while (n != 0);
return arr;
}
private static uint PadTo (uint value, uint align)
{
uint newValue = AlignTo(value, align);
return newValue - value;
}
private static uint AlignTo (uint value, uint align)
{
return (value + (align - 1)) & ~(align - 1);
}
}