using System.Buffers.Binary; using System.Collections.Immutable; using System.Reflection.PortableExecutable; namespace RobotNet10.ScriptEditor.Helpers.Webcil; /// /// Reads a .NET assembly in a normal PE COFF file and writes it out as a Webcil file /// public class WebcilConverter { // Interesting stuff we've learned about the input PE file public record PEFileInfo( // The sections in the PE file ImmutableArray 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 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 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.Builder headerBuilder = ImmutableArray.CreateBuilder(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 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 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(Stream s, T structure) where T : unmanaged { unsafe { byte* p = (byte*)&structure; s.Write(new ReadOnlySpan(p, sizeof(T))); } } #else private static void WriteStructure(Stream s, T structure) where T : unmanaged { int size = Marshal.SizeOf(); 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 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 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 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 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 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 FixupDebugDirectoryEntries(PEFileInfo peInfo, WCFileInfo wcInfo) { int dataPointerAdjustment = peInfo.SectionStart.Position - wcInfo.SectionStart.Position; ImmutableArray entries = peInfo.DebugDirectoryEntries; ImmutableArray.Builder newEntries = ImmutableArray.CreateBuilder(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 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); } }