diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index a2d7b5bb48..7fe6df907a 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -147,6 +147,7 @@ + diff --git a/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs b/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs index 8a7df4beef..90f44096a8 100644 --- a/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs +++ b/ICSharpCode.Decompiler/Metadata/MetadataExtensions.cs @@ -1,6 +1,25 @@ -using System; +// Copyright (c) 2018 Siegfried Pammer +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; using System.Buffers.Binary; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Metadata; @@ -426,5 +445,23 @@ public static BlobReader AsBlobReader(this MetadataReader metadataReader) return new(metadataReader.MetadataPointer, metadataReader.MetadataLength); } } + + public static uint ReadULEB128(this BinaryReader reader) + { + uint val = 0; + int shift = 0; + while (true) + { + byte b = reader.ReadByte(); + val |= (b & 0b0111_1111u) << shift; + if ((b & 0b1000_0000) == 0) + break; + shift += 7; + if (shift >= 35) + throw new OverflowException(); + } + return val; + } + } } diff --git a/ICSharpCode.Decompiler/Metadata/MetadataFile.cs b/ICSharpCode.Decompiler/Metadata/MetadataFile.cs index ba884731b3..c92c0e061c 100644 --- a/ICSharpCode.Decompiler/Metadata/MetadataFile.cs +++ b/ICSharpCode.Decompiler/Metadata/MetadataFile.cs @@ -51,6 +51,7 @@ public enum MetadataFileKind { PortableExecutable, ProgramDebugDatabase, + WebCIL, Metadata } diff --git a/ICSharpCode.Decompiler/Metadata/WebCilFile.cs b/ICSharpCode.Decompiler/Metadata/WebCilFile.cs new file mode 100644 index 0000000000..291316085c --- /dev/null +++ b/ICSharpCode.Decompiler/Metadata/WebCilFile.cs @@ -0,0 +1,284 @@ +// Copyright (c) 2024 Siegfried Pammer +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.MemoryMappedFiles; +using System.Reflection.Metadata; +using System.Text; + +using ICSharpCode.Decompiler.TypeSystem; + +#nullable enable + +namespace ICSharpCode.Decompiler.Metadata +{ + public class WebCilFile : MetadataFile, IDisposable, IModuleReference + { + readonly MemoryMappedViewAccessor view; + readonly long webcilOffset; + + private WebCilFile(string fileName, long webcilOffset, long metadataOffset, MemoryMappedViewAccessor view, ImmutableArray sectionHeaders, ImmutableArray wasmSections, MetadataReaderProvider provider, MetadataReaderOptions metadataOptions = MetadataReaderOptions.Default) + : base(MetadataFileKind.WebCIL, fileName, provider, metadataOptions, 0) + { + this.webcilOffset = webcilOffset; + this.MetadataOffset = (int)metadataOffset; + this.view = view; + this.SectionHeaders = sectionHeaders; + this.WasmSections = wasmSections; + } + + public static WebCilFile? FromStream(string fileName, MetadataReaderOptions metadataOptions = MetadataReaderOptions.Default) + { + using var memoryMappedFile = MemoryMappedFile.CreateFromFile(fileName, FileMode.Open, null, 0, MemoryMappedFileAccess.Read); + var view = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read); + try + { + // read magic "\0asm" + if (view.ReadUInt32(0) != WASM_MAGIC) + return null; + + // read version + if (view.ReadUInt32(4) != 1) + return null; + + using var stream = view.AsStream(); + using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); + + stream.Position += 8; + + long metadataOffset = -1; + List sections = new List(); + + while (stream.Position < stream.Length) + { + WasmSectionId id = (WasmSectionId)reader.ReadByte(); + uint size = reader.ReadULEB128(); + sections.Add(new WasmSection(id, stream.Position, size, view)); + + if (id == WasmSectionId.Custom && size == 0) + { + break; + } + stream.Seek(size, SeekOrigin.Current); + } + + foreach (var section in sections) + { + if (section.Id != WasmSectionId.Data || metadataOffset > -1) + continue; + + stream.Seek(section.Offset, SeekOrigin.Begin); + + uint numSegments = reader.ReadULEB128(); + if (numSegments != 2) + continue; + + // skip the first segment + if (reader.ReadByte() != 1) + continue; + + long segmentLength = reader.ReadULEB128(); + long segmentStart = reader.BaseStream.Position; + + reader.BaseStream.Seek(segmentLength, SeekOrigin.Current); + + if (reader.ReadByte() != 1) + continue; + + segmentLength = reader.ReadULEB128(); + if (TryReadWebCilSegment(reader, out var header, out metadataOffset, out var webcilOffset, out var sectionHeaders)) + { + stream.Seek(metadataOffset, SeekOrigin.Begin); + var metadata = MetadataReaderProvider.FromMetadataStream(stream, MetadataStreamOptions.LeaveOpen | MetadataStreamOptions.PrefetchMetadata); + + var result = new WebCilFile(fileName, webcilOffset, metadataOffset, view, ImmutableArray.Create(sectionHeaders), sections.ToImmutableArray(), metadata, metadataOptions); + + view = null; // don't dispose the view, we're still using it in the sections + return result; + } + } + + return null; + } + finally + { + view?.Dispose(); + } + } + + static unsafe bool TryReadWebCilSegment(BinaryReader reader, out WebcilHeader webcilHeader, out long metadataOffset, out long webcilOffset, [NotNullWhen(true)] out SectionHeader[]? sectionHeaders) + { + webcilHeader = default; + metadataOffset = -1; + sectionHeaders = null; + + webcilOffset = reader.BaseStream.Position; + + if (reader.ReadUInt32() != WEBCIL_MAGIC) + return false; + + webcilHeader.VersionMajor = reader.ReadUInt16(); + webcilHeader.VersionMinor = reader.ReadUInt16(); + webcilHeader.CoffSections = reader.ReadUInt16(); + _ = reader.ReadUInt16(); // reserved0 + webcilHeader.PECliHeaderRVA = reader.ReadUInt32(); + webcilHeader.PECliHeaderSize = reader.ReadUInt32(); + webcilHeader.PEDebugRVA = reader.ReadUInt32(); + webcilHeader.PEDebugSize = reader.ReadUInt32(); + + sectionHeaders = new SectionHeader[webcilHeader.CoffSections]; + for (int i = 0; i < webcilHeader.CoffSections; i++) + { + sectionHeaders[i].VirtualSize = reader.ReadUInt32(); + sectionHeaders[i].VirtualAddress = reader.ReadUInt32(); + sectionHeaders[i].RawDataSize = reader.ReadUInt32(); + sectionHeaders[i].RawDataPtr = reader.ReadUInt32(); + } + + long corHeaderStart = TranslateRVA(sectionHeaders, webcilOffset, webcilHeader.PECliHeaderRVA); + if (reader.BaseStream.Seek(corHeaderStart, SeekOrigin.Begin) != corHeaderStart) + return false; + int byteCount = reader.ReadInt32(); + int majorVersion = reader.ReadUInt16(); + int minorVersion = reader.ReadUInt16(); + metadataOffset = TranslateRVA(sectionHeaders, webcilOffset, (uint)reader.ReadInt32()); + return reader.BaseStream.Seek(metadataOffset, SeekOrigin.Begin) == metadataOffset; + } + + public override int MetadataOffset { get; } + + private static int GetContainingSectionIndex(IEnumerable sections, int rva) + { + int i = 0; + foreach (var section in sections) + { + if (rva >= section.VirtualAddress && rva < section.VirtualAddress + section.VirtualSize) + { + return i; + } + i++; + } + return -1; + } + + private static long TranslateRVA(IEnumerable sections, long webcilOffset, uint rva) + { + foreach (var section in sections) + { + if (rva >= section.VirtualAddress && rva < section.VirtualAddress + section.VirtualSize) + { + return section.RawDataPtr + (rva - section.VirtualAddress) + webcilOffset; + } + } + throw new BadImageFormatException("RVA not found in any section"); + } + + public override MethodBodyBlock GetMethodBody(int rva) + { + var reader = GetSectionData(rva).GetReader(); + return MethodBodyBlock.Create(reader); + } + + public override int GetContainingSectionIndex(int rva) + { + return GetContainingSectionIndex(SectionHeaders, rva); + } + + public override unsafe SectionData GetSectionData(int rva) + { + foreach (var section in SectionHeaders) + { + if (rva >= section.VirtualAddress && rva < section.VirtualAddress + section.VirtualSize) + { + byte* ptr = (byte*)0; + view.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); + return new SectionData(ptr + section.RawDataPtr + webcilOffset + (rva - section.VirtualAddress), (int)section.RawDataSize); + } + } + throw new BadImageFormatException("RVA not found in any section"); + } + + public override ImmutableArray SectionHeaders { get; } + + public ImmutableArray WasmSections { get; } + + IModule? IModuleReference.Resolve(ITypeResolveContext context) + { + return new MetadataModule(context.Compilation, this, TypeSystemOptions.Default); + } + + public void Dispose() + { + view.Dispose(); + } + + public struct WebcilHeader + { + public ushort VersionMajor; + public ushort VersionMinor; + public ushort CoffSections; + public uint PECliHeaderRVA; + public uint PECliHeaderSize; + public uint PEDebugRVA; + public uint PEDebugSize; + } + + const uint WASM_MAGIC = 0x6d736100u; // "\0asm" + const uint WEBCIL_MAGIC = 0x4c496257u; // "WbIL" + + [DebuggerDisplay("WasmSection {Id}: {Offset} {Size}")] + public class WasmSection + { + public WasmSectionId Id; + public long Offset; + public uint Size; + private MemoryMappedViewAccessor view; + + public WasmSection(WasmSectionId id, long offset, uint size, MemoryMappedViewAccessor view) + { + this.Id = id; + this.Size = size; + this.Offset = offset; + this.view = view; + } + } + + public enum WasmSectionId : byte + { + // order matters: enum values must match the WebAssembly spec + Custom = 0, + Type = 1, + Import = 2, + Function = 3, + Table = 4, + Memory = 5, + Global = 6, + Export = 7, + Start = 8, + Element = 9, + Code = 10, + Data = 11, + DataCount = 12, + } + } +} diff --git a/ICSharpCode.Decompiler/SRMExtensions.cs b/ICSharpCode.Decompiler/SRMExtensions.cs index a86e455253..e461272d0f 100644 --- a/ICSharpCode.Decompiler/SRMExtensions.cs +++ b/ICSharpCode.Decompiler/SRMExtensions.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Immutable; +using System.IO; +using System.IO.MemoryMappedFiles; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; @@ -748,5 +750,11 @@ public static string ToILSyntax(this SignatureCallingConvention callConv) _ => callConv.ToString().ToLowerInvariant() }; } + + public static UnmanagedMemoryStream AsStream(this MemoryMappedViewAccessor view) + { + long size = checked((long)view.SafeMemoryMappedViewHandle.ByteLength); + return new UnmanagedMemoryStream(view.SafeMemoryMappedViewHandle, 0, size); + } } } diff --git a/ICSharpCode.Decompiler/SingleFileBundle.cs b/ICSharpCode.Decompiler/SingleFileBundle.cs index c92af8b352..b898639edd 100644 --- a/ICSharpCode.Decompiler/SingleFileBundle.cs +++ b/ICSharpCode.Decompiler/SingleFileBundle.cs @@ -104,18 +104,12 @@ public struct Entry public string RelativePath; // Path of an embedded file, relative to the Bundle source-directory. } - static UnmanagedMemoryStream AsStream(MemoryMappedViewAccessor view) - { - long size = checked((long)view.SafeMemoryMappedViewHandle.ByteLength); - return new UnmanagedMemoryStream(view.SafeMemoryMappedViewHandle, 0, size); - } - /// /// Reads the manifest header from the memory mapping. /// public static Header ReadManifest(MemoryMappedViewAccessor view, long bundleHeaderOffset) { - using var stream = AsStream(view); + using var stream = view.AsStream(); stream.Seek(bundleHeaderOffset, SeekOrigin.Begin); return ReadManifest(stream); } diff --git a/ICSharpCode.ILSpyX/LoadedAssembly.cs b/ICSharpCode.ILSpyX/LoadedAssembly.cs index 6ae72866f6..19509df007 100644 --- a/ICSharpCode.ILSpyX/LoadedAssembly.cs +++ b/ICSharpCode.ILSpyX/LoadedAssembly.cs @@ -383,6 +383,16 @@ async Task LoadAsync(Task? streamTask) bundle.LoadedAssembly = this; return new LoadResult(loadAssemblyException, bundle); } + // If it's not a .NET module, maybe it's a WASM module + var wasm = WebCilFile.FromStream(fileName); + if (wasm != null) + { + lock (loadedAssemblies) + { + loadedAssemblies.Add(wasm, this); + } + return new LoadResult(loadAssemblyException, wasm); + } // If it's not a .NET module, maybe it's a zip archive (e.g. .nupkg) try { diff --git a/ILSpy/Images/Images.cs b/ILSpy/Images/Images.cs index 5b48f10bc8..49dfde0fd6 100644 --- a/ILSpy/Images/Images.cs +++ b/ILSpy/Images/Images.cs @@ -59,6 +59,7 @@ static ImageSource Load(string icon) public static readonly ImageSource ReferenceFolder = Load("ReferenceFolder"); public static readonly ImageSource NuGet = Load(null, "Images/NuGet.png"); public static readonly ImageSource MetadataFile = Load("MetadataFile"); + public static readonly ImageSource WebAssemblyFile = Load("WebAssembly"); public static readonly ImageSource ProgramDebugDatabase = Load("ProgramDebugDatabase"); public static readonly ImageSource Metadata = Load("Metadata"); diff --git a/ILSpy/Images/WebAssembly.svg b/ILSpy/Images/WebAssembly.svg new file mode 100644 index 0000000000..f2d67d77a3 --- /dev/null +++ b/ILSpy/Images/WebAssembly.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/ILSpy/Images/WebAssembly.xaml b/ILSpy/Images/WebAssembly.xaml new file mode 100644 index 0000000000..302c838256 --- /dev/null +++ b/ILSpy/Images/WebAssembly.xaml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ILSpy/MainWindow.xaml.cs b/ILSpy/MainWindow.xaml.cs index cd8dc1c441..c42facb8f2 100644 --- a/ILSpy/MainWindow.xaml.cs +++ b/ILSpy/MainWindow.xaml.cs @@ -1385,7 +1385,7 @@ void OpenCommandExecuted(object sender, ExecutedRoutedEventArgs e) { e.Handled = true; OpenFileDialog dlg = new OpenFileDialog(); - dlg.Filter = ".NET assemblies|*.dll;*.exe;*.winmd|Nuget Packages (*.nupkg)|*.nupkg|Portable Program Database (*.pdb)|*.pdb|All files|*.*"; + dlg.Filter = ".NET assemblies|*.dll;*.exe;*.winmd;*.wasm|Nuget Packages (*.nupkg)|*.nupkg|Portable Program Database (*.pdb)|*.pdb|All files|*.*"; dlg.Multiselect = true; dlg.RestoreDirectory = true; if (dlg.ShowDialog() == true) diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index 7d4c2dd5f1..112b01cc42 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -103,6 +103,7 @@ public override object Icon { return loadResult.MetadataFile.Kind switch { MetadataFile.MetadataFileKind.PortableExecutable => Images.Assembly, MetadataFile.MetadataFileKind.ProgramDebugDatabase => Images.ProgramDebugDatabase, + MetadataFile.MetadataFileKind.WebCIL => Images.WebAssemblyFile, _ => Images.MetadataFile, }; } @@ -203,6 +204,9 @@ protected override void LoadChildren() case MetadataFile.MetadataFileKind.PortableExecutable: LoadChildrenForPEFile(loadResult.PEFile); break; + case MetadataFile.MetadataFileKind.WebCIL: + LoadChildrenForWebCilFile((WebCilFile)loadResult.MetadataFile); + break; default: var metadata = loadResult.MetadataFile; this.Children.Add(new MetadataTablesTreeNode(metadata)); @@ -291,6 +295,72 @@ NamespaceTreeNode GetOrCreateNamespaceTreeNode(string @namespace) } } + void LoadChildrenForWebCilFile(WebCilFile module) + { + typeSystem = LoadedAssembly.GetTypeSystemOrNull(); + var assembly = (MetadataModule)typeSystem.MainModule; + this.Children.Add(new MetadataTreeNode(module, Resources.Metadata)); + Decompiler.DebugInfo.IDebugInfoProvider debugInfo = LoadedAssembly.GetDebugInfoOrNull(); + if (debugInfo is PortableDebugInfoProvider ppdb + && ppdb.GetMetadataReader() is System.Reflection.Metadata.MetadataReader reader) + { + this.Children.Add(new MetadataTreeNode(ppdb.ToMetadataFile(), $"Debug Metadata ({(ppdb.IsEmbedded ? "Embedded" : "From portable PDB")})")); + } + this.Children.Add(new ReferenceFolderTreeNode(module, this)); + if (module.Resources.Any()) + this.Children.Add(new ResourceListTreeNode(module)); + foreach (NamespaceTreeNode ns in namespaces.Values) + { + ns.Children.Clear(); + } + namespaces.Clear(); + bool useNestedStructure = MainWindow.Instance.CurrentDisplaySettings.UseNestedNamespaceNodes; + foreach (var type in assembly.TopLevelTypeDefinitions.OrderBy(t => t.ReflectionName, NaturalStringComparer.Instance)) + { + var ns = GetOrCreateNamespaceTreeNode(type.Namespace); + TypeTreeNode node = new TypeTreeNode(type, this); + typeDict[(TypeDefinitionHandle)type.MetadataToken] = node; + ns.Children.Add(node); + } + foreach (NamespaceTreeNode ns in namespaces.Values + .Where(ns => ns.Children.Count > 0 && ns.Parent == null) + .OrderBy(n => n.Name, NaturalStringComparer.Instance)) + { + this.Children.Add(ns); + SetPublicAPI(ns); + } + + NamespaceTreeNode GetOrCreateNamespaceTreeNode(string @namespace) + { + if (!namespaces.TryGetValue(@namespace, out NamespaceTreeNode ns)) + { + if (useNestedStructure) + { + int decimalIndex = @namespace.LastIndexOf('.'); + if (decimalIndex < 0) + { + var escapedNamespace = Language.EscapeName(@namespace); + ns = new NamespaceTreeNode(escapedNamespace); + } + else + { + var parentNamespaceTreeNode = GetOrCreateNamespaceTreeNode(@namespace.Substring(0, decimalIndex)); + var escapedInnerNamespace = Language.EscapeName(@namespace.Substring(decimalIndex + 1)); + ns = new NamespaceTreeNode(escapedInnerNamespace); + parentNamespaceTreeNode.Children.Add(ns); + } + } + else + { + var escapedNamespace = Language.EscapeName(@namespace); + ns = new NamespaceTreeNode(escapedNamespace); + } + namespaces.Add(@namespace, ns); + } + return ns; + } + } + private static void SetPublicAPI(NamespaceTreeNode ns) { foreach (NamespaceTreeNode innerNamespace in ns.Children.OfType())