Initial commit

This commit is contained in:
2026-07-03 16:31:37 +07:00
commit 899c7c637d
1939 changed files with 641750 additions and 0 deletions

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);
}
}