From 1efe20622abd82c6db5c72f74348122af5f42407 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 7 Jul 2026 14:44:45 -0700 Subject: [PATCH 1/2] Add Wasm module support --- README.md | 16 +- ...Dotsider-Core-Analysis-AssemblyAnalyzer.md | 30 +- .../Dotsider-Core-Analysis-AssemblyLoader.md | 7 +- ...alysis-Models-AssemblyOpenResult-Direct.md | 10 +- ...otsider-Core-Analysis-Models-BinaryKind.md | 15 +- ...Core-Analysis-Models-NativeSymbolSource.md | 11 + ...ore-Analysis-Models-WasmDataSegmentInfo.md | 85 ++ ...-Analysis-Models-WasmElementSegmentInfo.md | 96 ++ ...der-Core-Analysis-Models-WasmExportInfo.md | 74 ++ ...r-Core-Analysis-Models-WasmExternalKind.md | 80 ++ ...r-Core-Analysis-Models-WasmFunctionInfo.md | 217 ++++ ...der-Core-Analysis-Models-WasmGlobalInfo.md | 85 ++ ...der-Core-Analysis-Models-WasmImportInfo.md | 96 ++ ...ider-Core-Analysis-Models-WasmLocalInfo.md | 74 ++ ...der-Core-Analysis-Models-WasmMemoryInfo.md | 96 ++ ...der-Core-Analysis-Models-WasmModuleInfo.md | 301 +++++ ...er-Core-Analysis-Models-WasmSectionInfo.md | 85 ++ ...ore-Analysis-Models-WasmSymbolMapStatus.md | 50 + ...ider-Core-Analysis-Models-WasmTableInfo.md | 85 ++ ...tsider-Core-Analysis-Models-WasmTagInfo.md | 74 ++ ...sider-Core-Analysis-Models-WasmTypeInfo.md | 74 ++ ...otsider-Core-Analysis-Models-WebcilInfo.md | 122 ++ .../docs/api/Dotsider-Core-Analysis-Models.md | 134 ++- .../docs/api/Dotsider-Core-Analysis.md | 7 +- ...tsider-Core-Protocol-WasmPayloadBuilder.md | 80 ++ ...ider-Core-Protocol-WebcilPayloadBuilder.md | 43 + .../docs/api/Dotsider-Core-Protocol.md | 20 + docs/src/content/docs/reference/cli.md | 12 +- docs/src/content/docs/reference/mcp.md | 8 +- docs/src/content/docs/usage/general.md | 6 + docs/src/content/docs/usage/hex-dump.md | 2 + docs/src/content/docs/usage/il-inspector.md | 8 +- docs/src/content/docs/usage/pe-metadata.md | 14 +- docs/src/content/docs/usage/size-map.md | 6 + samples/README.md | 5 +- samples/WasmConsole/Program.cs | 22 + samples/WasmConsole/WasmConsole.csproj | 12 + .../Analysis/AssemblyAnalyzer.cs | 237 +++- src/Dotsider.Core/Analysis/AssemblyLoader.cs | 5 +- .../Analysis/Disasm/NativeDisassembler.cs | 30 +- .../Analysis/Models/AssemblyOpenResult.cs | 4 +- .../Analysis/Models/BinaryKind.cs | 6 + .../Analysis/Models/NativeSymbolSource.cs | 8 +- .../Analysis/Models/WasmDataSegmentInfo.cs | 10 + .../Analysis/Models/WasmElementSegmentInfo.cs | 16 + .../Analysis/Models/WasmExportInfo.cs | 9 + .../Analysis/Models/WasmExternalKind.cs | 25 + .../Analysis/Models/WasmFunctionInfo.cs | 38 + .../Analysis/Models/WasmGlobalInfo.cs | 14 + .../Analysis/Models/WasmImportInfo.cs | 16 + .../Analysis/Models/WasmLocalInfo.cs | 9 + .../Analysis/Models/WasmMemoryInfo.cs | 16 + .../Analysis/Models/WasmModuleInfo.cs | 59 + .../Analysis/Models/WasmSectionInfo.cs | 14 + .../Analysis/Models/WasmSymbolMapStatus.cs | 16 + .../Analysis/Models/WasmTableInfo.cs | 14 + .../Analysis/Models/WasmTagInfo.cs | 12 + .../Analysis/Models/WasmTypeInfo.cs | 12 + .../Analysis/Models/WebcilInfo.cs | 22 + src/Dotsider.Core/Analysis/SizeAnalyzer.cs | 108 ++ .../Analysis/Wasm/WasmDisassembler.cs | 230 ++++ .../Analysis/Wasm/WasmModuleReader.cs | 1019 +++++++++++++++++ .../Analysis/Wasm/WasmSymbolBuilder.cs | 61 + .../Analysis/Wasm/WebcilCodeViewData.cs | 11 + .../Analysis/Wasm/WebcilDebugEntry.cs | 24 + .../Analysis/Wasm/WebcilImageReader.cs | 602 ++++++++++ .../Protocol/WasmPayloadBuilder.cs | 132 +++ .../Protocol/WebcilPayloadBuilder.cs | 34 + src/Dotsider.Core/README.md | 8 +- src/Dotsider.Mcp/README.md | 17 +- src/Dotsider.Mcp/Tools/AssemblyTools.cs | 6 +- src/Dotsider.Mcp/Tools/SymbolTools.cs | 7 +- src/Dotsider.Mcp/Tools/ToolHelpers.cs | 8 +- src/Dotsider.Mcp/Tools/WasmTools.cs | 85 ++ src/Dotsider/Commands/AnalyzeCommand.cs | 32 +- .../DotsiderDiagnosticsListener.cs | 32 +- src/Dotsider/Views/GeneralView.cs | 53 +- src/Dotsider/Views/IlInspectorView.cs | 110 ++ src/Dotsider/Views/PeMetadataView.cs | 759 +++++++++++- .../Dotsider.Mcp.Tests/AssemblyToolsTests.cs | 93 ++ .../SampleAssemblyFixture.cs | 85 ++ tests/Dotsider.Mcp.Tests/SymbolToolsTests.cs | 131 ++- .../ToolRegistrationTests.cs | 4 + tests/Dotsider.Mcp.Tests/WasmToolsTests.cs | 96 ++ tests/Dotsider.Tests/CliTests.cs | 158 ++- tests/Dotsider.Tests/DotsiderStateTests.cs | 30 + tests/Dotsider.Tests/PeMetadataViewTests.cs | 61 + tests/Dotsider.Tests/SampleAssemblyFixture.cs | 51 +- tests/Dotsider.Tests/SessionBundleTests.cs | 35 + tests/Dotsider.Tests/SessionSymbolTests.cs | 58 + tests/Dotsider.Tests/StandardModeViewTests.cs | 122 +- tests/Dotsider.Tests/TestHelpers.cs | 10 +- .../WasmSdkModuleDecoderTests.cs | 322 ++++-- 93 files changed, 7130 insertions(+), 218 deletions(-) create mode 100644 docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmDataSegmentInfo.md create mode 100644 docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmElementSegmentInfo.md create mode 100644 docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmExportInfo.md create mode 100644 docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmExternalKind.md create mode 100644 docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmFunctionInfo.md create mode 100644 docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmGlobalInfo.md create mode 100644 docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmImportInfo.md create mode 100644 docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmLocalInfo.md create mode 100644 docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmMemoryInfo.md create mode 100644 docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmModuleInfo.md create mode 100644 docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmSectionInfo.md create mode 100644 docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmSymbolMapStatus.md create mode 100644 docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmTableInfo.md create mode 100644 docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmTagInfo.md create mode 100644 docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmTypeInfo.md create mode 100644 docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WebcilInfo.md create mode 100644 docs/src/content/docs/api/Dotsider-Core-Protocol-WasmPayloadBuilder.md create mode 100644 docs/src/content/docs/api/Dotsider-Core-Protocol-WebcilPayloadBuilder.md create mode 100644 samples/WasmConsole/Program.cs create mode 100644 samples/WasmConsole/WasmConsole.csproj create mode 100644 src/Dotsider.Core/Analysis/Models/WasmDataSegmentInfo.cs create mode 100644 src/Dotsider.Core/Analysis/Models/WasmElementSegmentInfo.cs create mode 100644 src/Dotsider.Core/Analysis/Models/WasmExportInfo.cs create mode 100644 src/Dotsider.Core/Analysis/Models/WasmExternalKind.cs create mode 100644 src/Dotsider.Core/Analysis/Models/WasmFunctionInfo.cs create mode 100644 src/Dotsider.Core/Analysis/Models/WasmGlobalInfo.cs create mode 100644 src/Dotsider.Core/Analysis/Models/WasmImportInfo.cs create mode 100644 src/Dotsider.Core/Analysis/Models/WasmLocalInfo.cs create mode 100644 src/Dotsider.Core/Analysis/Models/WasmMemoryInfo.cs create mode 100644 src/Dotsider.Core/Analysis/Models/WasmModuleInfo.cs create mode 100644 src/Dotsider.Core/Analysis/Models/WasmSectionInfo.cs create mode 100644 src/Dotsider.Core/Analysis/Models/WasmSymbolMapStatus.cs create mode 100644 src/Dotsider.Core/Analysis/Models/WasmTableInfo.cs create mode 100644 src/Dotsider.Core/Analysis/Models/WasmTagInfo.cs create mode 100644 src/Dotsider.Core/Analysis/Models/WasmTypeInfo.cs create mode 100644 src/Dotsider.Core/Analysis/Models/WebcilInfo.cs create mode 100644 src/Dotsider.Core/Analysis/Wasm/WasmDisassembler.cs create mode 100644 src/Dotsider.Core/Analysis/Wasm/WasmModuleReader.cs create mode 100644 src/Dotsider.Core/Analysis/Wasm/WasmSymbolBuilder.cs create mode 100644 src/Dotsider.Core/Analysis/Wasm/WebcilCodeViewData.cs create mode 100644 src/Dotsider.Core/Analysis/Wasm/WebcilDebugEntry.cs create mode 100644 src/Dotsider.Core/Analysis/Wasm/WebcilImageReader.cs create mode 100644 src/Dotsider.Core/Protocol/WasmPayloadBuilder.cs create mode 100644 src/Dotsider.Core/Protocol/WebcilPayloadBuilder.cs create mode 100644 src/Dotsider.Mcp/Tools/WasmTools.cs create mode 100644 tests/Dotsider.Mcp.Tests/WasmToolsTests.cs diff --git a/README.md b/README.md index 91cb7131..58bebea9 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Grab a standalone binary from [Releases](https://github.com/willibrandon/dotside ## What it does -dotsider opens any .NET DLL or EXE and lets you explore it across 8 tabs. Apphost executables are handled as first-class inputs: when an `.exe` has no .NET metadata, dotsider offers to open the companion managed `.dll`. Self-contained single-file apps work too — dotsider reads the bundle, extracts the entry assembly, and analyzes it directly. +dotsider opens .NET DLLs, EXEs, and supported `.wasm` outputs and lets you explore them across 8 tabs. Apphost executables are handled as first-class inputs: when an `.exe` has no .NET metadata, dotsider offers to open the companion managed `.dll`. Self-contained single-file apps work too — dotsider reads the bundle, extracts the entry assembly, and analyzes it directly. Native AOT binaries get native treatment instead of an empty metadata view. dotsider validates the embedded ReadyToRun header, walks the runtime sections, surfaces imports, exports, load config, recovered AOT types, recovered methods, and frozen string literals, then renders native disassembly for every .NET architecture it recognizes: x64, Arm64, x86, Arm32/Thumb-2, RISC-V64, LoongArch64, and Wasm32. @@ -49,11 +49,13 @@ When the Native AOT build tree is available, dotsider can attach the pre-ILC man ReadyToRun (crossgen2) images keep their full managed metadata, and dotsider joins that metadata to the precompiled method bodies in the same file or composite image. You can see IL beside native code, named call targets from import tables, and component assemblies followed in both directions. +Browser-wasm outputs split into two useful views. Point dotsider at `dotnet.native.wasm` and it parses the runtime module's Wasm sections, type/table/memory/global declarations, imports, exports, element and data segments, function bodies, and `dotnet.native.js.symbols`, then renders Wasm32 disassembly with direct-call targets and typed operands named from the same indexes the runtime uses. Point it at a Webcil app assembly such as `WasmConsole.wasm` and it unwraps the managed metadata and IL instead of treating the container as runtime code. + | Tab | What you see | |-----|-------------| | **1 General** | Assembly identity, target framework, architecture, dependency table. Press Enter on a reference to drill into it. | -| **2 PE/Metadata** | COFF headers, CLR header, sections, TypeDefs, MethodDefs, AssemblyRefs, custom attributes, resources, debug directory, native imports, exports, load config, ReadyToRun sections, and recovered AOT types. Press `g` on a TypeDef or MethodDef to jump to its IL. | -| **3 IL / Native** | The label reflects the loaded image: **IL Inspector** for managed IL, **Disassembly** for native-only views, and **IL + Native** when IL is paired with native code. Managed assemblies show a namespace/type/method tree with IL disassembly, PDB source spans, Source Link markers, locals, go-to-definition, and hex jumps. Native AOT binaries list recovered functions and render real native disassembly for x64, Arm64, x86, Arm32/Thumb-2, RISC-V64, LoongArch64, and Wasm32 with named call/branch/data targets, `Foo+0x12`, `loc_…` labels, and `MODULE!Function` imports. ReadyToRun images keep the managed tree, mark precompiled methods, and show IL beside native ranges (hot, funclets, cold). Attached pre-ILC sidecars do the same for Native AOT methods. | +| **2 PE/Metadata** | COFF headers, CLR header, PE/Wasm sections, TypeDefs, MethodDefs, AssemblyRefs, custom attributes, resources, debug directory, native imports, exports, load config, ReadyToRun sections, recovered AOT types, and native/Wasm symbols. Press `g` on a TypeDef or MethodDef to jump to its IL. | +| **3 IL / Native** | The label reflects the loaded image: **IL Inspector** for managed IL, **Disassembly** for native-only views, and **IL + Native** when IL is paired with native code. Managed assemblies show a namespace/type/method tree with IL disassembly, PDB source spans, Source Link markers, locals, go-to-definition, and hex jumps. Native AOT binaries and raw Wasm modules list recovered functions and render real disassembly for x64, Arm64, x86, Arm32/Thumb-2, RISC-V64, LoongArch64, and Wasm32 with named call/branch/data targets, `Foo+0x12`, `loc_…` labels, `MODULE!Function` imports, and Wasm function-index calls. ReadyToRun images keep the managed tree, mark precompiled methods, and show IL beside native ranges (hot, funclets, cold). Attached pre-ILC sidecars do the same for Native AOT methods. | | **4 Strings** | User strings, metadata strings, raw ASCII and UTF-16 binary scans, and frozen AOT string literals, with configurable minimum length. | | **5 Hex Dump** | Hex editor with vi-style modal editing (read-only by default), byte category coloring, data interpretation panel, jump-to-offset, and vim navigation. | | **6 Dep Graph** | Visual dependency graph — your assembly at the root, references as nodes, edge weights by TypeRef count. Press Enter on a node to open that assembly. | @@ -81,7 +83,7 @@ The binary lands at `src/Dotsider/bin/Debug/net10.0/dotsider`. ## Usage ``` -dotsider # TUI mode — interactive assembly explorer +dotsider # TUI mode — interactive assembly/native module explorer dotsider # TUI mode — browse NuGet package contents dotsider diff # TUI mode — assembly comparison; AOT size diff for mstat inputs @@ -113,6 +115,8 @@ dotsider analyze MyAotApp.exe --correlate # correlate with pre-ILC assembl dotsider analyze MyAotApp.exe --correlate Type.Method # IL and native code side by side (name or 0xVA) dotsider analyze MyR2RApp.dll --r2r-correlate # ReadyToRun stats (precompiled methods, composite) dotsider analyze MyR2RApp.dll --r2r-correlate Type.Method # IL beside precompiled native (name or 0xVA) +dotsider analyze dotnet.native.wasm --symbols # list SDK Wasm function symbols +dotsider analyze dotnet.native.wasm --disasm 0x1234 # disassemble a Wasm function body dotsider analyze MyLib.dll --embedded-source Type.Method # print embedded source dotsider analyze MyLib.dll --deps # assembly references dotsider analyze MyLib.dll --strings # extract strings @@ -224,8 +228,8 @@ dotsider starts with the low-level APIs that ship with .NET, then layers its own - **`System.Reflection.Metadata`** provides `MetadataReader` for traversing ECMA-335 metadata tables (types, methods, references, custom attributes, string heaps) - **`System.Reflection.PortableExecutable`** provides `PEReader` for PE structure (COFF header, sections, CLR header, method bodies) -- Custom Native AOT and ReadyToRun readers parse RTR section tables, recovered NativeFormat metadata, frozen objects, mstat/DGML sidecars, native symbols, and precompiled method maps -- From-scratch native decoders render AOT and R2R code for x64, Arm64, x86, Arm32/Thumb-2, RISC-V64, LoongArch64, and Wasm32 +- Custom Native AOT, ReadyToRun, WebAssembly, and Webcil readers parse RTR section tables, recovered NativeFormat metadata, frozen objects, mstat/DGML sidecars, native symbols, precompiled method maps, Wasm standard/custom sections, function bodies, SDK symbol maps, and managed `.wasm` app payloads +- From-scratch native decoders render AOT, R2R, native, and Wasm code for x64, Arm64, x86, Arm32/Thumb-2, RISC-V64, LoongArch64, and Wasm32 - **`System.IO.Compression`** handles NuGet packages (which are just ZIP files containing a `.nuspec` manifest and DLLs) The dynamic analysis tab uses `Microsoft.Diagnostics.NETCore.Client` to connect to a running .NET process via EventPipe — the same diagnostic infrastructure that powers `dotnet-trace` and `dotnet-counters`. It launches your assembly with a reverse-connect diagnostic port, so events are captured from the very first instruction. diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-AssemblyAnalyzer.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-AssemblyAnalyzer.md index 892452b7..8e1ca3b9 100644 --- a/docs/src/content/docs/api/Dotsider-Core-Analysis-AssemblyAnalyzer.md +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-AssemblyAnalyzer.md @@ -1,6 +1,6 @@ --- title: "AssemblyAnalyzer" -description: "Core analyzer that reads a .NET assembly and extracts PE, metadata, IL, and string information. Uses PEReader and MetadataReader from the BCL." +description: "Core analyzer that reads .NET assemblies, Webcil app assemblies, native binaries, and raw Wasm modules. It uses BCL metadata/PE readers where possible and routes runtime-native formats through dotsider's format readers for IL, strings, symbols, disassembly, and size data." slug: api/dotsider.core.analysis.assemblyanalyzer sidebar: order: 0 @@ -10,8 +10,9 @@ sidebar: **Assembly:** Dotsider.Core.dll -Core analyzer that reads a .NET assembly and extracts PE, metadata, IL, and string information. -Uses [PEReader](https://learn.microsoft.com/dotnet/api/system.reflection.portableexecutable.pereader) and [MetadataReader](https://learn.microsoft.com/dotnet/api/system.reflection.metadata.metadatareader) from the BCL. +Core analyzer that reads .NET assemblies, Webcil app assemblies, native binaries, and raw Wasm +modules. It uses BCL metadata/PE readers where possible and routes runtime-native formats +through dotsider's format readers for IL, strings, symbols, disassembly, and size data. ```csharp public sealed class AssemblyAnalyzer : IDisposable @@ -726,6 +727,29 @@ Gets the TypeRef metadata table entries. public IReadOnlyList TypeRefs { get; } ``` +### WasmModuleInfo + +Parsed WebAssembly module facts when this file is a raw `.wasm` module, or null for +PE, ELF, and Mach-O inputs. The main .NET browser-wasm native module is +`dotnet.native.wasm`. + +**Returns:** [WasmModuleInfo](/api/dotsider.core.analysis.models.wasmmoduleinfo/) + +```csharp +public WasmModuleInfo? WasmModuleInfo { get; } +``` + +### WebcilInfo + +Parsed Webcil provenance when this analyzer opened a Webcil managed assembly directly or +unwrapped one from a WebAssembly container. Null for PE, raw Wasm, ELF, and Mach-O inputs. + +**Returns:** [WebcilInfo](/api/dotsider.core.analysis.models.webcilinfo/) + +```csharp +public WebcilInfo? WebcilInfo { get; } +``` + ## Methods ### AttachPreIlcCompanions() diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-AssemblyLoader.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-AssemblyLoader.md index f0c7c570..e53da712 100644 --- a/docs/src/content/docs/api/Dotsider-Core-Analysis-AssemblyLoader.md +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-AssemblyLoader.md @@ -1,6 +1,6 @@ --- title: "AssemblyLoader" -description: "Shared factory for opening assembly files. Handles apphosts (companion .dll redirect), single-file bundles (entry assembly extraction), Native AOT binaries, and direct .dll/.exe loading. Returns an AssemblyOpenResult that preserves the distinction so callers can decide how to present each case (e.g. showing an apphost dialog)." +description: "Shared factory for opening assembly files. Handles apphosts (companion .dll redirect), single-file bundles (entry assembly extraction), Native AOT binaries, raw Wasm modules, and direct .dll/.exe loading. Returns an AssemblyOpenResult that preserves the distinction so callers can decide how to present each case (e.g. showing an apphost dialog)." slug: api/dotsider.core.analysis.assemblyloader sidebar: order: 0 @@ -11,7 +11,7 @@ sidebar: **Assembly:** Dotsider.Core.dll Shared factory for opening assembly files. Handles apphosts (companion .dll redirect), -single-file bundles (entry assembly extraction), Native AOT binaries, and direct +single-file bundles (entry assembly extraction), Native AOT binaries, raw Wasm modules, and direct .dll/.exe loading. Returns an [AssemblyOpenResult](/api/dotsider.core.analysis.models.assemblyopenresult/) that preserves the distinction so callers can decide how to present each case (e.g. showing an apphost dialog). @@ -39,7 +39,8 @@ An [AssemblyOpenResult](/api/dotsider.core.analysis.models.assemblyopenresult/) [Direct](/api/dotsider.core.analysis.models.assemblyopenresult.direct/) for regular assemblies, [ApphostWithCompanion](/api/dotsider.core.analysis.models.assemblyopenresult.apphostwithcompanion/) for native apphosts with a companion .dll, [BundleEntry](/api/dotsider.core.analysis.models.assemblyopenresult.bundleentry/) for single-file bundles, -or [NativeAot](/api/dotsider.core.analysis.models.assemblyopenresult.nativeaot/) for Native AOT compiled binaries. +[NativeAot](/api/dotsider.core.analysis.models.assemblyopenresult.nativeaot/) for Native AOT compiled binaries, +or [Direct](/api/dotsider.core.analysis.models.assemblyopenresult.direct/) for raw Wasm modules. ```csharp public static AssemblyOpenResult Open(string filePath) diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-AssemblyOpenResult-Direct.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-AssemblyOpenResult-Direct.md index 1ce05a53..1ed0a865 100644 --- a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-AssemblyOpenResult-Direct.md +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-AssemblyOpenResult-Direct.md @@ -1,6 +1,6 @@ --- title: "AssemblyOpenResult.Direct" -description: "Direct load — the file is a .dll or .exe with metadata, or a native binary with no metadata and no ReadyToRun header (unknown format)." +description: "Direct load — the file is a .dll or .exe with metadata, a raw WebAssembly module, or a native binary with no metadata and no ReadyToRun header (unknown format)." slug: api/dotsider.core.analysis.models.assemblyopenresult.direct sidebar: order: 2 @@ -10,8 +10,8 @@ sidebar: **Assembly:** Dotsider.Core.dll -Direct load — the file is a .dll or .exe with metadata, or a native binary -with no metadata and no ReadyToRun header (unknown format). +Direct load — the file is a .dll or .exe with metadata, a raw WebAssembly module, +or a native binary with no metadata and no ReadyToRun header (unknown format). ```csharp public sealed record AssemblyOpenResult.Direct : AssemblyOpenResult, IEquatable, IEquatable @@ -30,8 +30,8 @@ public sealed record AssemblyOpenResult.Direct : AssemblyOpenResult, IEquatable< ### Direct(AssemblyAnalyzer) -Direct load — the file is a .dll or .exe with metadata, or a native binary -with no metadata and no ReadyToRun header (unknown format). +Direct load — the file is a .dll or .exe with metadata, a raw WebAssembly module, +or a native binary with no metadata and no ReadyToRun header (unknown format). **Parameters:** diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-BinaryKind.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-BinaryKind.md index 63ca9040..57a4a2dc 100644 --- a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-BinaryKind.md +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-BinaryKind.md @@ -35,7 +35,7 @@ A native binary with no CLR metadata and no ReadyToRun header (apphost, unknown **Returns:** [BinaryKind](/api/dotsider.core.analysis.models.binarykind/) ```csharp -Native = 3 +Native = 4 ``` ### NativeAot @@ -46,7 +46,7 @@ whose image embeds a validated ReadyToRun header. **Returns:** [BinaryKind](/api/dotsider.core.analysis.models.binarykind/) ```csharp -NativeAot = 2 +NativeAot = 3 ``` ### ReadyToRun @@ -61,3 +61,14 @@ the native bodies are additionally correlated to their managed methods. ReadyToRun = 1 ``` +### Wasm + +A WebAssembly module, most commonly the `dotnet.native.wasm` runtime module from a +.NET browser-wasm publish. It has native Wasm code but no ECMA-335 metadata of its own. + +**Returns:** [BinaryKind](/api/dotsider.core.analysis.models.binarykind/) + +```csharp +Wasm = 2 +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-NativeSymbolSource.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-NativeSymbolSource.md index fb3f56e3..dc04036b 100644 --- a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-NativeSymbolSource.md +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-NativeSymbolSource.md @@ -101,3 +101,14 @@ A crossgen2 ReadyToRun image's method entry-point tables: named, sized function ReadyToRun = 7 ``` +### WebAssembly + +A WebAssembly module's function/code sections, optionally named from +`dotnet.native.js.symbols`, the Wasm name section, or exports. + +**Returns:** [NativeSymbolSource](/api/dotsider.core.analysis.models.nativesymbolsource/) + +```csharp +WebAssembly = 8 +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmDataSegmentInfo.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmDataSegmentInfo.md new file mode 100644 index 00000000..9f6ee246 --- /dev/null +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmDataSegmentInfo.md @@ -0,0 +1,85 @@ +--- +title: "WasmDataSegmentInfo" +description: "One WebAssembly data segment." +slug: api/dotsider.core.analysis.models.wasmdatasegmentinfo +sidebar: + order: 2 +--- + +**Namespace:** `Dotsider.Core.Analysis.Models` + +**Assembly:** Dotsider.Core.dll + +One WebAssembly data segment. + +```csharp +public sealed record WasmDataSegmentInfo : IEquatable +``` + +## Inheritance + +[Object](https://learn.microsoft.com/dotnet/api/system.object) → **WasmDataSegmentInfo** + +## Implements + +- [IEquatable\](https://learn.microsoft.com/dotnet/api/system.iequatable-1) + +## Constructors + +### WasmDataSegmentInfo(int, string, long, int) + +One WebAssembly data segment. + +**Parameters:** + +- `Index` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The data segment index. +- `Mode` ([String](https://learn.microsoft.com/dotnet/api/system.string)): The decoded segment mode: active, passive, or active-explicit-memory. +- `FileOffset` ([Int64](https://learn.microsoft.com/dotnet/api/system.int64)): The file offset where the segment's bytes begin. +- `Size` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The segment byte size. + +```csharp +public WasmDataSegmentInfo(int Index, string Mode, long FileOffset, int Size) +``` + +## Properties + +### FileOffset + +The file offset where the segment's bytes begin. + +**Returns:** [Int64](https://learn.microsoft.com/dotnet/api/system.int64) + +```csharp +public long FileOffset { get; init; } +``` + +### Index + +The data segment index. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int Index { get; init; } +``` + +### Mode + +The decoded segment mode: active, passive, or active-explicit-memory. + +**Returns:** [String](https://learn.microsoft.com/dotnet/api/system.string) + +```csharp +public string Mode { get; init; } +``` + +### Size + +The segment byte size. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int Size { get; init; } +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmElementSegmentInfo.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmElementSegmentInfo.md new file mode 100644 index 00000000..1653de71 --- /dev/null +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmElementSegmentInfo.md @@ -0,0 +1,96 @@ +--- +title: "WasmElementSegmentInfo" +description: "One WebAssembly element segment declaration." +slug: api/dotsider.core.analysis.models.wasmelementsegmentinfo +sidebar: + order: 2 +--- + +**Namespace:** `Dotsider.Core.Analysis.Models` + +**Assembly:** Dotsider.Core.dll + +One WebAssembly element segment declaration. + +```csharp +public sealed record WasmElementSegmentInfo : IEquatable +``` + +## Inheritance + +[Object](https://learn.microsoft.com/dotnet/api/system.object) → **WasmElementSegmentInfo** + +## Implements + +- [IEquatable\](https://learn.microsoft.com/dotnet/api/system.iequatable-1) + +## Constructors + +### WasmElementSegmentInfo(int, string, int?, string, int) + +One WebAssembly element segment declaration. + +**Parameters:** + +- `Index` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The zero-based element segment index. +- `Mode` ([String](https://learn.microsoft.com/dotnet/api/system.string)): The decoded element segment mode. +- `TableIndex` ([Nullable\](https://learn.microsoft.com/dotnet/api/system.nullable-1)): The table index when the mode records one. +- `ElementType` ([String](https://learn.microsoft.com/dotnet/api/system.string)): The reference type or element kind. +- `ElementCount` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The number of recorded element expressions or indices. + +```csharp +public WasmElementSegmentInfo(int Index, string Mode, int? TableIndex, string ElementType, int ElementCount) +``` + +## Properties + +### ElementCount + +The number of recorded element expressions or indices. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int ElementCount { get; init; } +``` + +### ElementType + +The reference type or element kind. + +**Returns:** [String](https://learn.microsoft.com/dotnet/api/system.string) + +```csharp +public string ElementType { get; init; } +``` + +### Index + +The zero-based element segment index. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int Index { get; init; } +``` + +### Mode + +The decoded element segment mode. + +**Returns:** [String](https://learn.microsoft.com/dotnet/api/system.string) + +```csharp +public string Mode { get; init; } +``` + +### TableIndex + +The table index when the mode records one. + +**Returns:** [Nullable\](https://learn.microsoft.com/dotnet/api/system.nullable-1) + +```csharp +public int? TableIndex { get; init; } +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmExportInfo.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmExportInfo.md new file mode 100644 index 00000000..c92cdc84 --- /dev/null +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmExportInfo.md @@ -0,0 +1,74 @@ +--- +title: "WasmExportInfo" +description: "One WebAssembly export entry." +slug: api/dotsider.core.analysis.models.wasmexportinfo +sidebar: + order: 2 +--- + +**Namespace:** `Dotsider.Core.Analysis.Models` + +**Assembly:** Dotsider.Core.dll + +One WebAssembly export entry. + +```csharp +public sealed record WasmExportInfo : IEquatable +``` + +## Inheritance + +[Object](https://learn.microsoft.com/dotnet/api/system.object) → **WasmExportInfo** + +## Implements + +- [IEquatable\](https://learn.microsoft.com/dotnet/api/system.iequatable-1) + +## Constructors + +### WasmExportInfo(string, WasmExternalKind, int) + +One WebAssembly export entry. + +**Parameters:** + +- `Name` ([String](https://learn.microsoft.com/dotnet/api/system.string)): The exported name. +- `Kind` ([WasmExternalKind](/api/dotsider.core.analysis.models.wasmexternalkind/)): The exported external kind. +- `Index` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The exported index in the kind's index space. + +```csharp +public WasmExportInfo(string Name, WasmExternalKind Kind, int Index) +``` + +## Properties + +### Index + +The exported index in the kind's index space. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int Index { get; init; } +``` + +### Kind + +The exported external kind. + +**Returns:** [WasmExternalKind](/api/dotsider.core.analysis.models.wasmexternalkind/) + +```csharp +public WasmExternalKind Kind { get; init; } +``` + +### Name + +The exported name. + +**Returns:** [String](https://learn.microsoft.com/dotnet/api/system.string) + +```csharp +public string Name { get; init; } +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmExternalKind.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmExternalKind.md new file mode 100644 index 00000000..ede6e8cc --- /dev/null +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmExternalKind.md @@ -0,0 +1,80 @@ +--- +title: "WasmExternalKind" +description: "The external item kind used by WebAssembly import and export sections." +slug: api/dotsider.core.analysis.models.wasmexternalkind +sidebar: + order: 2 +--- + +**Namespace:** `Dotsider.Core.Analysis.Models` + +**Assembly:** Dotsider.Core.dll + +The external item kind used by WebAssembly import and export sections. + +```csharp +public enum WasmExternalKind +``` + +## Fields + +### Function + +A function index. + +**Returns:** [WasmExternalKind](/api/dotsider.core.analysis.models.wasmexternalkind/) + +```csharp +Function = 0 +``` + +### Global + +A global index. + +**Returns:** [WasmExternalKind](/api/dotsider.core.analysis.models.wasmexternalkind/) + +```csharp +Global = 3 +``` + +### Memory + +A memory index. + +**Returns:** [WasmExternalKind](/api/dotsider.core.analysis.models.wasmexternalkind/) + +```csharp +Memory = 2 +``` + +### Table + +A table index. + +**Returns:** [WasmExternalKind](/api/dotsider.core.analysis.models.wasmexternalkind/) + +```csharp +Table = 1 +``` + +### Tag + +An exception tag index. + +**Returns:** [WasmExternalKind](/api/dotsider.core.analysis.models.wasmexternalkind/) + +```csharp +Tag = 4 +``` + +### Unknown + +An unrecognized external kind. + +**Returns:** [WasmExternalKind](/api/dotsider.core.analysis.models.wasmexternalkind/) + +```csharp +Unknown = 5 +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmFunctionInfo.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmFunctionInfo.md new file mode 100644 index 00000000..98774a69 --- /dev/null +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmFunctionInfo.md @@ -0,0 +1,217 @@ +--- +title: "WasmFunctionInfo" +description: "A WebAssembly function, imported or defined in the code section." +slug: api/dotsider.core.analysis.models.wasmfunctioninfo +sidebar: + order: 2 +--- + +**Namespace:** `Dotsider.Core.Analysis.Models` + +**Assembly:** Dotsider.Core.dll + +A WebAssembly function, imported or defined in the code section. + +```csharp +public sealed record WasmFunctionInfo : IEquatable +``` + +## Inheritance + +[Object](https://learn.microsoft.com/dotnet/api/system.object) → **WasmFunctionInfo** + +## Implements + +- [IEquatable\](https://learn.microsoft.com/dotnet/api/system.iequatable-1) + +## Constructors + +### WasmFunctionInfo(int, int?, string, string, bool, string?, string?, bool, IReadOnlyList\, long?, int, long?, int, IReadOnlyList\, IReadOnlyList\, IReadOnlyList\) + +A WebAssembly function, imported or defined in the code section. + +**Parameters:** + +- `Index` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The module-wide function index, including imported functions. +- `TypeIndex` ([Nullable\](https://learn.microsoft.com/dotnet/api/system.nullable-1)): The function type index, when known. +- `Name` ([String](https://learn.microsoft.com/dotnet/api/system.string)): The best display name for the function. +- `NameSource` ([String](https://learn.microsoft.com/dotnet/api/system.string)): Where the display name came from. +- `IsImported` ([Boolean](https://learn.microsoft.com/dotnet/api/system.boolean)): Whether the function is imported and has no body in this module. +- `ImportModule` ([String](https://learn.microsoft.com/dotnet/api/system.string)): The import module for imported functions. +- `ImportName` ([String](https://learn.microsoft.com/dotnet/api/system.string)): The import name for imported functions. +- `IsExported` ([Boolean](https://learn.microsoft.com/dotnet/api/system.boolean)): Whether the function is exported. +- `ExportNames` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): All export names that point at this function. +- `BodyOffset` ([Nullable\](https://learn.microsoft.com/dotnet/api/system.nullable-1)): The file offset of the function body payload, including local declarations. +- `BodySize` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The body payload size in bytes. +- `CodeOffset` ([Nullable\](https://learn.microsoft.com/dotnet/api/system.nullable-1)): The file offset of the first instruction byte after local declarations. +- `CodeSize` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The instruction byte count after local declarations. +- `Locals` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): The function's run-length encoded local declarations. +- `ParamTypes` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): The raw Wasm parameter type bytes. +- `ResultTypes` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): The raw Wasm result type bytes. + +```csharp +public WasmFunctionInfo(int Index, int? TypeIndex, string Name, string NameSource, bool IsImported, string? ImportModule, string? ImportName, bool IsExported, IReadOnlyList ExportNames, long? BodyOffset, int BodySize, long? CodeOffset, int CodeSize, IReadOnlyList Locals, IReadOnlyList ParamTypes, IReadOnlyList ResultTypes) +``` + +## Properties + +### BodyOffset + +The file offset of the function body payload, including local declarations. + +**Returns:** [Nullable\](https://learn.microsoft.com/dotnet/api/system.nullable-1) + +```csharp +public long? BodyOffset { get; init; } +``` + +### BodySize + +The body payload size in bytes. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int BodySize { get; init; } +``` + +### CodeOffset + +The file offset of the first instruction byte after local declarations. + +**Returns:** [Nullable\](https://learn.microsoft.com/dotnet/api/system.nullable-1) + +```csharp +public long? CodeOffset { get; init; } +``` + +### CodeSize + +The instruction byte count after local declarations. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int CodeSize { get; init; } +``` + +### ExportNames + +All export names that point at this function. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList ExportNames { get; init; } +``` + +### ImportModule + +The import module for imported functions. + +**Returns:** [String](https://learn.microsoft.com/dotnet/api/system.string) + +```csharp +public string? ImportModule { get; init; } +``` + +### ImportName + +The import name for imported functions. + +**Returns:** [String](https://learn.microsoft.com/dotnet/api/system.string) + +```csharp +public string? ImportName { get; init; } +``` + +### Index + +The module-wide function index, including imported functions. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int Index { get; init; } +``` + +### IsExported + +Whether the function is exported. + +**Returns:** [Boolean](https://learn.microsoft.com/dotnet/api/system.boolean) + +```csharp +public bool IsExported { get; init; } +``` + +### IsImported + +Whether the function is imported and has no body in this module. + +**Returns:** [Boolean](https://learn.microsoft.com/dotnet/api/system.boolean) + +```csharp +public bool IsImported { get; init; } +``` + +### Locals + +The function's run-length encoded local declarations. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList Locals { get; init; } +``` + +### Name + +The best display name for the function. + +**Returns:** [String](https://learn.microsoft.com/dotnet/api/system.string) + +```csharp +public string Name { get; init; } +``` + +### NameSource + +Where the display name came from. + +**Returns:** [String](https://learn.microsoft.com/dotnet/api/system.string) + +```csharp +public string NameSource { get; init; } +``` + +### ParamTypes + +The raw Wasm parameter type bytes. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList ParamTypes { get; init; } +``` + +### ResultTypes + +The raw Wasm result type bytes. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList ResultTypes { get; init; } +``` + +### TypeIndex + +The function type index, when known. + +**Returns:** [Nullable\](https://learn.microsoft.com/dotnet/api/system.nullable-1) + +```csharp +public int? TypeIndex { get; init; } +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmGlobalInfo.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmGlobalInfo.md new file mode 100644 index 00000000..f2a1250e --- /dev/null +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmGlobalInfo.md @@ -0,0 +1,85 @@ +--- +title: "WasmGlobalInfo" +description: "One WebAssembly global declaration." +slug: api/dotsider.core.analysis.models.wasmglobalinfo +sidebar: + order: 2 +--- + +**Namespace:** `Dotsider.Core.Analysis.Models` + +**Assembly:** Dotsider.Core.dll + +One WebAssembly global declaration. + +```csharp +public sealed record WasmGlobalInfo : IEquatable +``` + +## Inheritance + +[Object](https://learn.microsoft.com/dotnet/api/system.object) → **WasmGlobalInfo** + +## Implements + +- [IEquatable\](https://learn.microsoft.com/dotnet/api/system.iequatable-1) + +## Constructors + +### WasmGlobalInfo(int, byte, string, bool) + +One WebAssembly global declaration. + +**Parameters:** + +- `Index` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The zero-based global index. +- `ValueType` ([Byte](https://learn.microsoft.com/dotnet/api/system.byte)): The raw WebAssembly value-type byte. +- `ValueTypeName` ([String](https://learn.microsoft.com/dotnet/api/system.string)): The display name for the value type. +- `IsMutable` ([Boolean](https://learn.microsoft.com/dotnet/api/system.boolean)): Whether the global is mutable. + +```csharp +public WasmGlobalInfo(int Index, byte ValueType, string ValueTypeName, bool IsMutable) +``` + +## Properties + +### Index + +The zero-based global index. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int Index { get; init; } +``` + +### IsMutable + +Whether the global is mutable. + +**Returns:** [Boolean](https://learn.microsoft.com/dotnet/api/system.boolean) + +```csharp +public bool IsMutable { get; init; } +``` + +### ValueType + +The raw WebAssembly value-type byte. + +**Returns:** [Byte](https://learn.microsoft.com/dotnet/api/system.byte) + +```csharp +public byte ValueType { get; init; } +``` + +### ValueTypeName + +The display name for the value type. + +**Returns:** [String](https://learn.microsoft.com/dotnet/api/system.string) + +```csharp +public string ValueTypeName { get; init; } +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmImportInfo.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmImportInfo.md new file mode 100644 index 00000000..8f1d451d --- /dev/null +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmImportInfo.md @@ -0,0 +1,96 @@ +--- +title: "WasmImportInfo" +description: "One WebAssembly import entry." +slug: api/dotsider.core.analysis.models.wasmimportinfo +sidebar: + order: 2 +--- + +**Namespace:** `Dotsider.Core.Analysis.Models` + +**Assembly:** Dotsider.Core.dll + +One WebAssembly import entry. + +```csharp +public sealed record WasmImportInfo : IEquatable +``` + +## Inheritance + +[Object](https://learn.microsoft.com/dotnet/api/system.object) → **WasmImportInfo** + +## Implements + +- [IEquatable\](https://learn.microsoft.com/dotnet/api/system.iequatable-1) + +## Constructors + +### WasmImportInfo(string, string, WasmExternalKind, int, int?) + +One WebAssembly import entry. + +**Parameters:** + +- `ModuleName` ([String](https://learn.microsoft.com/dotnet/api/system.string)): The imported module name. +- `Name` ([String](https://learn.microsoft.com/dotnet/api/system.string)): The imported item name. +- `Kind` ([WasmExternalKind](/api/dotsider.core.analysis.models.wasmexternalkind/)): The imported external kind. +- `Index` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The import's index within its index space when applicable. +- `TypeIndex` ([Nullable\](https://learn.microsoft.com/dotnet/api/system.nullable-1)): The function type index for function imports, or null. + +```csharp +public WasmImportInfo(string ModuleName, string Name, WasmExternalKind Kind, int Index, int? TypeIndex) +``` + +## Properties + +### Index + +The import's index within its index space when applicable. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int Index { get; init; } +``` + +### Kind + +The imported external kind. + +**Returns:** [WasmExternalKind](/api/dotsider.core.analysis.models.wasmexternalkind/) + +```csharp +public WasmExternalKind Kind { get; init; } +``` + +### ModuleName + +The imported module name. + +**Returns:** [String](https://learn.microsoft.com/dotnet/api/system.string) + +```csharp +public string ModuleName { get; init; } +``` + +### Name + +The imported item name. + +**Returns:** [String](https://learn.microsoft.com/dotnet/api/system.string) + +```csharp +public string Name { get; init; } +``` + +### TypeIndex + +The function type index for function imports, or null. + +**Returns:** [Nullable\](https://learn.microsoft.com/dotnet/api/system.nullable-1) + +```csharp +public int? TypeIndex { get; init; } +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmLocalInfo.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmLocalInfo.md new file mode 100644 index 00000000..b23d721d --- /dev/null +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmLocalInfo.md @@ -0,0 +1,74 @@ +--- +title: "WasmLocalInfo" +description: "A run-length encoded local declaration inside a WebAssembly function body." +slug: api/dotsider.core.analysis.models.wasmlocalinfo +sidebar: + order: 2 +--- + +**Namespace:** `Dotsider.Core.Analysis.Models` + +**Assembly:** Dotsider.Core.dll + +A run-length encoded local declaration inside a WebAssembly function body. + +```csharp +public sealed record WasmLocalInfo : IEquatable +``` + +## Inheritance + +[Object](https://learn.microsoft.com/dotnet/api/system.object) → **WasmLocalInfo** + +## Implements + +- [IEquatable\](https://learn.microsoft.com/dotnet/api/system.iequatable-1) + +## Constructors + +### WasmLocalInfo(uint, byte, string) + +A run-length encoded local declaration inside a WebAssembly function body. + +**Parameters:** + +- `Count` ([UInt32](https://learn.microsoft.com/dotnet/api/system.uint32)): The number of locals in this run. +- `ValueType` ([Byte](https://learn.microsoft.com/dotnet/api/system.byte)): The raw Wasm value type byte. +- `DisplayType` ([String](https://learn.microsoft.com/dotnet/api/system.string)): The display name of the value type. + +```csharp +public WasmLocalInfo(uint Count, byte ValueType, string DisplayType) +``` + +## Properties + +### Count + +The number of locals in this run. + +**Returns:** [UInt32](https://learn.microsoft.com/dotnet/api/system.uint32) + +```csharp +public uint Count { get; init; } +``` + +### DisplayType + +The display name of the value type. + +**Returns:** [String](https://learn.microsoft.com/dotnet/api/system.string) + +```csharp +public string DisplayType { get; init; } +``` + +### ValueType + +The raw Wasm value type byte. + +**Returns:** [Byte](https://learn.microsoft.com/dotnet/api/system.byte) + +```csharp +public byte ValueType { get; init; } +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmMemoryInfo.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmMemoryInfo.md new file mode 100644 index 00000000..40e3762e --- /dev/null +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmMemoryInfo.md @@ -0,0 +1,96 @@ +--- +title: "WasmMemoryInfo" +description: "One WebAssembly memory declaration." +slug: api/dotsider.core.analysis.models.wasmmemoryinfo +sidebar: + order: 2 +--- + +**Namespace:** `Dotsider.Core.Analysis.Models` + +**Assembly:** Dotsider.Core.dll + +One WebAssembly memory declaration. + +```csharp +public sealed record WasmMemoryInfo : IEquatable +``` + +## Inheritance + +[Object](https://learn.microsoft.com/dotnet/api/system.object) → **WasmMemoryInfo** + +## Implements + +- [IEquatable\](https://learn.microsoft.com/dotnet/api/system.iequatable-1) + +## Constructors + +### WasmMemoryInfo(int, ulong, ulong?, bool, bool) + +One WebAssembly memory declaration. + +**Parameters:** + +- `Index` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The zero-based memory index. +- `MinimumPages` ([UInt64](https://learn.microsoft.com/dotnet/api/system.uint64)): The minimum memory page count. +- `MaximumPages` ([Nullable\](https://learn.microsoft.com/dotnet/api/system.nullable-1)): The maximum memory page count when one is declared. +- `IsShared` ([Boolean](https://learn.microsoft.com/dotnet/api/system.boolean)): Whether the memory is declared shared. +- `IsMemory64` ([Boolean](https://learn.microsoft.com/dotnet/api/system.boolean)): Whether the memory uses 64-bit indices. + +```csharp +public WasmMemoryInfo(int Index, ulong MinimumPages, ulong? MaximumPages, bool IsShared, bool IsMemory64) +``` + +## Properties + +### Index + +The zero-based memory index. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int Index { get; init; } +``` + +### IsMemory64 + +Whether the memory uses 64-bit indices. + +**Returns:** [Boolean](https://learn.microsoft.com/dotnet/api/system.boolean) + +```csharp +public bool IsMemory64 { get; init; } +``` + +### IsShared + +Whether the memory is declared shared. + +**Returns:** [Boolean](https://learn.microsoft.com/dotnet/api/system.boolean) + +```csharp +public bool IsShared { get; init; } +``` + +### MaximumPages + +The maximum memory page count when one is declared. + +**Returns:** [Nullable\](https://learn.microsoft.com/dotnet/api/system.nullable-1) + +```csharp +public ulong? MaximumPages { get; init; } +``` + +### MinimumPages + +The minimum memory page count. + +**Returns:** [UInt64](https://learn.microsoft.com/dotnet/api/system.uint64) + +```csharp +public ulong MinimumPages { get; init; } +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmModuleInfo.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmModuleInfo.md new file mode 100644 index 00000000..9930aac6 --- /dev/null +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmModuleInfo.md @@ -0,0 +1,301 @@ +--- +title: "WasmModuleInfo" +description: "Parsed facts for a WebAssembly module, including its functions and optional .NET symbol map." +slug: api/dotsider.core.analysis.models.wasmmoduleinfo +sidebar: + order: 2 +--- + +**Namespace:** `Dotsider.Core.Analysis.Models` + +**Assembly:** Dotsider.Core.dll + +Parsed facts for a WebAssembly module, including its functions and optional .NET symbol map. + +```csharp +public sealed record WasmModuleInfo : IEquatable +``` + +## Inheritance + +[Object](https://learn.microsoft.com/dotnet/api/system.object) → **WasmModuleInfo** + +## Implements + +- [IEquatable\](https://learn.microsoft.com/dotnet/api/system.iequatable-1) + +## Constructors + +### WasmModuleInfo(int, IReadOnlyList\, IReadOnlyList\, IReadOnlyList\, IReadOnlyList\, IReadOnlyList\, IReadOnlyList\, IReadOnlyList\, IReadOnlyList\, IReadOnlyList\, IReadOnlyList\, IReadOnlyList\, int?, int?, IReadOnlyList\, IReadOnlyList\, string?, WasmSymbolMapStatus, int, string?) + +Parsed facts for a WebAssembly module, including its functions and optional .NET symbol map. + +**Parameters:** + +- `Version` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The WebAssembly binary version. +- `Sections` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): The section table in file order. +- `Types` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): The parsed function type entries. +- `Imports` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): The parsed import entries. +- `Exports` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): The parsed export entries. +- `Functions` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): Imported and defined functions in function-index order. +- `Tables` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): The parsed table declarations. +- `Memories` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): The parsed memory declarations. +- `Globals` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): The parsed global declarations. +- `Elements` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): The parsed element segments. +- `DataSegments` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): The parsed data segments. +- `Tags` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): The parsed exception tag declarations. +- `StartFunctionIndex` ([Nullable\](https://learn.microsoft.com/dotnet/api/system.nullable-1)): The start function index, when present. +- `DataCount` ([Nullable\](https://learn.microsoft.com/dotnet/api/system.nullable-1)): The data-count section value, when present. +- `TargetFeatures` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): Feature names from the `target_features` custom section. +- `ProducerFields` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): Producer strings from the `producers` custom section. +- `SymbolMapPath` ([String](https://learn.microsoft.com/dotnet/api/system.string)): The symbol-map sidecar path, when loaded. +- `SymbolMapStatus` ([WasmSymbolMapStatus](/api/dotsider.core.analysis.models.wasmsymbolmapstatus/)): The symbol-map probe outcome. +- `SymbolMapEntryCount` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The number of parsed sidecar entries. +- `Diagnostic` ([String](https://learn.microsoft.com/dotnet/api/system.string)): A diagnostic note when parsing had to degrade, or null. + +```csharp +public WasmModuleInfo(int Version, IReadOnlyList Sections, IReadOnlyList Types, IReadOnlyList Imports, IReadOnlyList Exports, IReadOnlyList Functions, IReadOnlyList Tables, IReadOnlyList Memories, IReadOnlyList Globals, IReadOnlyList Elements, IReadOnlyList DataSegments, IReadOnlyList Tags, int? StartFunctionIndex, int? DataCount, IReadOnlyList TargetFeatures, IReadOnlyList ProducerFields, string? SymbolMapPath, WasmSymbolMapStatus SymbolMapStatus, int SymbolMapEntryCount, string? Diagnostic) +``` + +## Properties + +### CodeSize + +The total byte count of all defined function instruction streams. + +**Returns:** [Int64](https://learn.microsoft.com/dotnet/api/system.int64) + +```csharp +public long CodeSize { get; } +``` + +### DataCount + +The data-count section value, when present. + +**Returns:** [Nullable\](https://learn.microsoft.com/dotnet/api/system.nullable-1) + +```csharp +public int? DataCount { get; init; } +``` + +### DataSegments + +The parsed data segments. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList DataSegments { get; init; } +``` + +### DataSize + +The total byte count of all data segments. + +**Returns:** [Int64](https://learn.microsoft.com/dotnet/api/system.int64) + +```csharp +public long DataSize { get; } +``` + +### DefinedFunctionCount + +The number of defined functions with code bodies in the module. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int DefinedFunctionCount { get; } +``` + +### Diagnostic + +A diagnostic note when parsing had to degrade, or null. + +**Returns:** [String](https://learn.microsoft.com/dotnet/api/system.string) + +```csharp +public string? Diagnostic { get; init; } +``` + +### Elements + +The parsed element segments. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList Elements { get; init; } +``` + +### Exports + +The parsed export entries. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList Exports { get; init; } +``` + +### Functions + +Imported and defined functions in function-index order. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList Functions { get; init; } +``` + +### Globals + +The parsed global declarations. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList Globals { get; init; } +``` + +### ImportedFunctionCount + +The number of imported functions in the module. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int ImportedFunctionCount { get; } +``` + +### Imports + +The parsed import entries. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList Imports { get; init; } +``` + +### Memories + +The parsed memory declarations. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList Memories { get; init; } +``` + +### ProducerFields + +Producer strings from the `producers` custom section. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList ProducerFields { get; init; } +``` + +### Sections + +The section table in file order. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList Sections { get; init; } +``` + +### StartFunctionIndex + +The start function index, when present. + +**Returns:** [Nullable\](https://learn.microsoft.com/dotnet/api/system.nullable-1) + +```csharp +public int? StartFunctionIndex { get; init; } +``` + +### SymbolMapEntryCount + +The number of parsed sidecar entries. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int SymbolMapEntryCount { get; init; } +``` + +### SymbolMapPath + +The symbol-map sidecar path, when loaded. + +**Returns:** [String](https://learn.microsoft.com/dotnet/api/system.string) + +```csharp +public string? SymbolMapPath { get; init; } +``` + +### SymbolMapStatus + +The symbol-map probe outcome. + +**Returns:** [WasmSymbolMapStatus](/api/dotsider.core.analysis.models.wasmsymbolmapstatus/) + +```csharp +public WasmSymbolMapStatus SymbolMapStatus { get; init; } +``` + +### Tables + +The parsed table declarations. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList Tables { get; init; } +``` + +### Tags + +The parsed exception tag declarations. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList Tags { get; init; } +``` + +### TargetFeatures + +Feature names from the `target_features` custom section. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList TargetFeatures { get; init; } +``` + +### Types + +The parsed function type entries. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList Types { get; init; } +``` + +### Version + +The WebAssembly binary version. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int Version { get; init; } +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmSectionInfo.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmSectionInfo.md new file mode 100644 index 00000000..dcb30df6 --- /dev/null +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmSectionInfo.md @@ -0,0 +1,85 @@ +--- +title: "WasmSectionInfo" +description: "One section in a WebAssembly module, including custom sections." +slug: api/dotsider.core.analysis.models.wasmsectioninfo +sidebar: + order: 2 +--- + +**Namespace:** `Dotsider.Core.Analysis.Models` + +**Assembly:** Dotsider.Core.dll + +One section in a WebAssembly module, including custom sections. + +```csharp +public sealed record WasmSectionInfo : IEquatable +``` + +## Inheritance + +[Object](https://learn.microsoft.com/dotnet/api/system.object) → **WasmSectionInfo** + +## Implements + +- [IEquatable\](https://learn.microsoft.com/dotnet/api/system.iequatable-1) + +## Constructors + +### WasmSectionInfo(byte, string, long, int) + +One section in a WebAssembly module, including custom sections. + +**Parameters:** + +- `Id` ([Byte](https://learn.microsoft.com/dotnet/api/system.byte)): The numeric section id. +- `Name` ([String](https://learn.microsoft.com/dotnet/api/system.string)): The standard section name, or the custom section name for id 0. +- `FileOffset` ([Int64](https://learn.microsoft.com/dotnet/api/system.int64)): The file offset where section payload bytes begin. +- `Size` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The section payload size in bytes. + +```csharp +public WasmSectionInfo(byte Id, string Name, long FileOffset, int Size) +``` + +## Properties + +### FileOffset + +The file offset where section payload bytes begin. + +**Returns:** [Int64](https://learn.microsoft.com/dotnet/api/system.int64) + +```csharp +public long FileOffset { get; init; } +``` + +### Id + +The numeric section id. + +**Returns:** [Byte](https://learn.microsoft.com/dotnet/api/system.byte) + +```csharp +public byte Id { get; init; } +``` + +### Name + +The standard section name, or the custom section name for id 0. + +**Returns:** [String](https://learn.microsoft.com/dotnet/api/system.string) + +```csharp +public string Name { get; init; } +``` + +### Size + +The section payload size in bytes. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int Size { get; init; } +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmSymbolMapStatus.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmSymbolMapStatus.md new file mode 100644 index 00000000..f30548b7 --- /dev/null +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmSymbolMapStatus.md @@ -0,0 +1,50 @@ +--- +title: "WasmSymbolMapStatus" +description: "The outcome of probing a WebAssembly module's dotnet.native.js.symbols sidecar." +slug: api/dotsider.core.analysis.models.wasmsymbolmapstatus +sidebar: + order: 2 +--- + +**Namespace:** `Dotsider.Core.Analysis.Models` + +**Assembly:** Dotsider.Core.dll + +The outcome of probing a WebAssembly module's `dotnet.native.js.symbols` sidecar. + +```csharp +public enum WasmSymbolMapStatus +``` + +## Fields + +### Corrupt + +The sidecar was found but no valid entries could be read. + +**Returns:** [WasmSymbolMapStatus](/api/dotsider.core.analysis.models.wasmsymbolmapstatus/) + +```csharp +Corrupt = 2 +``` + +### Loaded + +The sidecar was found and parsed. + +**Returns:** [WasmSymbolMapStatus](/api/dotsider.core.analysis.models.wasmsymbolmapstatus/) + +```csharp +Loaded = 1 +``` + +### Missing + +No sidecar was expected or found. + +**Returns:** [WasmSymbolMapStatus](/api/dotsider.core.analysis.models.wasmsymbolmapstatus/) + +```csharp +Missing = 0 +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmTableInfo.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmTableInfo.md new file mode 100644 index 00000000..7af3dc4e --- /dev/null +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmTableInfo.md @@ -0,0 +1,85 @@ +--- +title: "WasmTableInfo" +description: "One WebAssembly table declaration." +slug: api/dotsider.core.analysis.models.wasmtableinfo +sidebar: + order: 2 +--- + +**Namespace:** `Dotsider.Core.Analysis.Models` + +**Assembly:** Dotsider.Core.dll + +One WebAssembly table declaration. + +```csharp +public sealed record WasmTableInfo : IEquatable +``` + +## Inheritance + +[Object](https://learn.microsoft.com/dotnet/api/system.object) → **WasmTableInfo** + +## Implements + +- [IEquatable\](https://learn.microsoft.com/dotnet/api/system.iequatable-1) + +## Constructors + +### WasmTableInfo(int, string, ulong, ulong?) + +One WebAssembly table declaration. + +**Parameters:** + +- `Index` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The zero-based table index. +- `RefType` ([String](https://learn.microsoft.com/dotnet/api/system.string)): The table reference type. +- `Minimum` ([UInt64](https://learn.microsoft.com/dotnet/api/system.uint64)): The minimum element count. +- `Maximum` ([Nullable\](https://learn.microsoft.com/dotnet/api/system.nullable-1)): The maximum element count when one is declared. + +```csharp +public WasmTableInfo(int Index, string RefType, ulong Minimum, ulong? Maximum) +``` + +## Properties + +### Index + +The zero-based table index. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int Index { get; init; } +``` + +### Maximum + +The maximum element count when one is declared. + +**Returns:** [Nullable\](https://learn.microsoft.com/dotnet/api/system.nullable-1) + +```csharp +public ulong? Maximum { get; init; } +``` + +### Minimum + +The minimum element count. + +**Returns:** [UInt64](https://learn.microsoft.com/dotnet/api/system.uint64) + +```csharp +public ulong Minimum { get; init; } +``` + +### RefType + +The table reference type. + +**Returns:** [String](https://learn.microsoft.com/dotnet/api/system.string) + +```csharp +public string RefType { get; init; } +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmTagInfo.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmTagInfo.md new file mode 100644 index 00000000..78d0368d --- /dev/null +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmTagInfo.md @@ -0,0 +1,74 @@ +--- +title: "WasmTagInfo" +description: "One WebAssembly exception tag declaration." +slug: api/dotsider.core.analysis.models.wasmtaginfo +sidebar: + order: 2 +--- + +**Namespace:** `Dotsider.Core.Analysis.Models` + +**Assembly:** Dotsider.Core.dll + +One WebAssembly exception tag declaration. + +```csharp +public sealed record WasmTagInfo : IEquatable +``` + +## Inheritance + +[Object](https://learn.microsoft.com/dotnet/api/system.object) → **WasmTagInfo** + +## Implements + +- [IEquatable\](https://learn.microsoft.com/dotnet/api/system.iequatable-1) + +## Constructors + +### WasmTagInfo(int, uint, int) + +One WebAssembly exception tag declaration. + +**Parameters:** + +- `Index` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The zero-based tag index. +- `Attribute` ([UInt32](https://learn.microsoft.com/dotnet/api/system.uint32)): The tag attribute value. +- `TypeIndex` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The function type index used by the tag. + +```csharp +public WasmTagInfo(int Index, uint Attribute, int TypeIndex) +``` + +## Properties + +### Attribute + +The tag attribute value. + +**Returns:** [UInt32](https://learn.microsoft.com/dotnet/api/system.uint32) + +```csharp +public uint Attribute { get; init; } +``` + +### Index + +The zero-based tag index. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int Index { get; init; } +``` + +### TypeIndex + +The function type index used by the tag. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int TypeIndex { get; init; } +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmTypeInfo.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmTypeInfo.md new file mode 100644 index 00000000..c8945ed0 --- /dev/null +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WasmTypeInfo.md @@ -0,0 +1,74 @@ +--- +title: "WasmTypeInfo" +description: "One WebAssembly function type from the type section." +slug: api/dotsider.core.analysis.models.wasmtypeinfo +sidebar: + order: 2 +--- + +**Namespace:** `Dotsider.Core.Analysis.Models` + +**Assembly:** Dotsider.Core.dll + +One WebAssembly function type from the type section. + +```csharp +public sealed record WasmTypeInfo : IEquatable +``` + +## Inheritance + +[Object](https://learn.microsoft.com/dotnet/api/system.object) → **WasmTypeInfo** + +## Implements + +- [IEquatable\](https://learn.microsoft.com/dotnet/api/system.iequatable-1) + +## Constructors + +### WasmTypeInfo(int, IReadOnlyList\, IReadOnlyList\) + +One WebAssembly function type from the type section. + +**Parameters:** + +- `Index` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The zero-based type index. +- `ParamTypes` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): The raw WebAssembly parameter value-type bytes. +- `ResultTypes` ([IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1)): The raw WebAssembly result value-type bytes. + +```csharp +public WasmTypeInfo(int Index, IReadOnlyList ParamTypes, IReadOnlyList ResultTypes) +``` + +## Properties + +### Index + +The zero-based type index. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int Index { get; init; } +``` + +### ParamTypes + +The raw WebAssembly parameter value-type bytes. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList ParamTypes { get; init; } +``` + +### ResultTypes + +The raw WebAssembly result value-type bytes. + +**Returns:** [IReadOnlyList\](https://learn.microsoft.com/dotnet/api/system.collections.generic.ireadonlylist-1) + +```csharp +public IReadOnlyList ResultTypes { get; init; } +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WebcilInfo.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WebcilInfo.md new file mode 100644 index 00000000..09466c22 --- /dev/null +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models-WebcilInfo.md @@ -0,0 +1,122 @@ +--- +title: "WebcilInfo" +description: "Parsed provenance for a Webcil managed assembly, including whether it was wrapped inside a WebAssembly module. Webcil is a .NET metadata container used by browser-wasm publishes, so dotsider routes it through the managed metadata and IL experience." +slug: api/dotsider.core.analysis.models.webcilinfo +sidebar: + order: 2 +--- + +**Namespace:** `Dotsider.Core.Analysis.Models` + +**Assembly:** Dotsider.Core.dll + +Parsed provenance for a Webcil managed assembly, including whether it was wrapped inside +a WebAssembly module. Webcil is a .NET metadata container used by browser-wasm publishes, +so dotsider routes it through the managed metadata and IL experience. + +```csharp +public sealed record WebcilInfo : IEquatable +``` + +## Inheritance + +[Object](https://learn.microsoft.com/dotnet/api/system.object) → **WebcilInfo** + +## Implements + +- [IEquatable\](https://learn.microsoft.com/dotnet/api/system.iequatable-1) + +## Constructors + +### WebcilInfo(int, int, bool, long, int, int, int) + +Parsed provenance for a Webcil managed assembly, including whether it was wrapped inside +a WebAssembly module. Webcil is a .NET metadata container used by browser-wasm publishes, +so dotsider routes it through the managed metadata and IL experience. + +**Parameters:** + +- `VersionMajor` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The Webcil major format version. +- `VersionMinor` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The Webcil minor format version. +- `IsWasmWrapped` ([Boolean](https://learn.microsoft.com/dotnet/api/system.boolean)): True when the Webcil payload was found inside a Wasm wrapper module. +- `PayloadOffset` ([Int64](https://learn.microsoft.com/dotnet/api/system.int64)): The file offset of the Webcil payload in the opened file. +- `SectionCount` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The number of Webcil section records. +- `MetadataSize` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The size of the ECMA-335 metadata blob. +- `DebugDirectorySize` ([Int32](https://learn.microsoft.com/dotnet/api/system.int32)): The size of the Webcil debug directory, when present. + +```csharp +public WebcilInfo(int VersionMajor, int VersionMinor, bool IsWasmWrapped, long PayloadOffset, int SectionCount, int MetadataSize, int DebugDirectorySize) +``` + +## Properties + +### DebugDirectorySize + +The size of the Webcil debug directory, when present. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int DebugDirectorySize { get; init; } +``` + +### IsWasmWrapped + +True when the Webcil payload was found inside a Wasm wrapper module. + +**Returns:** [Boolean](https://learn.microsoft.com/dotnet/api/system.boolean) + +```csharp +public bool IsWasmWrapped { get; init; } +``` + +### MetadataSize + +The size of the ECMA-335 metadata blob. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int MetadataSize { get; init; } +``` + +### PayloadOffset + +The file offset of the Webcil payload in the opened file. + +**Returns:** [Int64](https://learn.microsoft.com/dotnet/api/system.int64) + +```csharp +public long PayloadOffset { get; init; } +``` + +### SectionCount + +The number of Webcil section records. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int SectionCount { get; init; } +``` + +### VersionMajor + +The Webcil major format version. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int VersionMajor { get; init; } +``` + +### VersionMinor + +The Webcil minor format version. + +**Returns:** [Int32](https://learn.microsoft.com/dotnet/api/system.int32) + +```csharp +public int VersionMinor { get; init; } +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models.md b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models.md index 86e6cdff..5e914d43 100644 --- a/docs/src/content/docs/api/Dotsider-Core-Analysis-Models.md +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis-Models.md @@ -57,8 +57,8 @@ public sealed record AssemblyOpenResult.BundleEntry : AssemblyOpenResult, IEquat ### [AssemblyOpenResult.Direct](/api/dotsider.core.analysis.models.assemblyopenresult.direct/) -Direct load — the file is a .dll or .exe with metadata, or a native binary -with no metadata and no ReadyToRun header (unknown format). +Direct load — the file is a .dll or .exe with metadata, a raw WebAssembly module, +or a native binary with no metadata and no ReadyToRun header (unknown format). ```csharp public sealed record AssemblyOpenResult.Direct : AssemblyOpenResult, IEquatable, IEquatable @@ -1089,6 +1089,120 @@ Information about a referenced type from the TypeRef metadata table. public sealed record TypeRefInfo : IEquatable ``` +### [WasmDataSegmentInfo](/api/dotsider.core.analysis.models.wasmdatasegmentinfo/) + +One WebAssembly data segment. + +```csharp +public sealed record WasmDataSegmentInfo : IEquatable +``` + +### [WasmElementSegmentInfo](/api/dotsider.core.analysis.models.wasmelementsegmentinfo/) + +One WebAssembly element segment declaration. + +```csharp +public sealed record WasmElementSegmentInfo : IEquatable +``` + +### [WasmExportInfo](/api/dotsider.core.analysis.models.wasmexportinfo/) + +One WebAssembly export entry. + +```csharp +public sealed record WasmExportInfo : IEquatable +``` + +### [WasmFunctionInfo](/api/dotsider.core.analysis.models.wasmfunctioninfo/) + +A WebAssembly function, imported or defined in the code section. + +```csharp +public sealed record WasmFunctionInfo : IEquatable +``` + +### [WasmGlobalInfo](/api/dotsider.core.analysis.models.wasmglobalinfo/) + +One WebAssembly global declaration. + +```csharp +public sealed record WasmGlobalInfo : IEquatable +``` + +### [WasmImportInfo](/api/dotsider.core.analysis.models.wasmimportinfo/) + +One WebAssembly import entry. + +```csharp +public sealed record WasmImportInfo : IEquatable +``` + +### [WasmLocalInfo](/api/dotsider.core.analysis.models.wasmlocalinfo/) + +A run-length encoded local declaration inside a WebAssembly function body. + +```csharp +public sealed record WasmLocalInfo : IEquatable +``` + +### [WasmMemoryInfo](/api/dotsider.core.analysis.models.wasmmemoryinfo/) + +One WebAssembly memory declaration. + +```csharp +public sealed record WasmMemoryInfo : IEquatable +``` + +### [WasmModuleInfo](/api/dotsider.core.analysis.models.wasmmoduleinfo/) + +Parsed facts for a WebAssembly module, including its functions and optional .NET symbol map. + +```csharp +public sealed record WasmModuleInfo : IEquatable +``` + +### [WasmSectionInfo](/api/dotsider.core.analysis.models.wasmsectioninfo/) + +One section in a WebAssembly module, including custom sections. + +```csharp +public sealed record WasmSectionInfo : IEquatable +``` + +### [WasmTableInfo](/api/dotsider.core.analysis.models.wasmtableinfo/) + +One WebAssembly table declaration. + +```csharp +public sealed record WasmTableInfo : IEquatable +``` + +### [WasmTagInfo](/api/dotsider.core.analysis.models.wasmtaginfo/) + +One WebAssembly exception tag declaration. + +```csharp +public sealed record WasmTagInfo : IEquatable +``` + +### [WasmTypeInfo](/api/dotsider.core.analysis.models.wasmtypeinfo/) + +One WebAssembly function type from the type section. + +```csharp +public sealed record WasmTypeInfo : IEquatable +``` + +### [WebcilInfo](/api/dotsider.core.analysis.models.webcilinfo/) + +Parsed provenance for a Webcil managed assembly, including whether it was wrapped inside +a WebAssembly module. Webcil is a .NET metadata container used by browser-wasm publishes, +so dotsider routes it through the managed metadata and IL experience. + +```csharp +public sealed record WebcilInfo : IEquatable +``` + ## Structs ### [MstatSectionPolicy](/api/dotsider.core.analysis.models.mstatsectionpolicy/) @@ -1458,3 +1572,19 @@ Current state of the traced process lifecycle. public enum TraceProcessState ``` +### [WasmExternalKind](/api/dotsider.core.analysis.models.wasmexternalkind/) + +The external item kind used by WebAssembly import and export sections. + +```csharp +public enum WasmExternalKind +``` + +### [WasmSymbolMapStatus](/api/dotsider.core.analysis.models.wasmsymbolmapstatus/) + +The outcome of probing a WebAssembly module's `dotnet.native.js.symbols` sidecar. + +```csharp +public enum WasmSymbolMapStatus +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Analysis.md b/docs/src/content/docs/api/Dotsider-Core-Analysis.md index 15c5e97c..19a64a96 100644 --- a/docs/src/content/docs/api/Dotsider-Core-Analysis.md +++ b/docs/src/content/docs/api/Dotsider-Core-Analysis.md @@ -17,8 +17,9 @@ public static class ApphostDetector ### [AssemblyAnalyzer](/api/dotsider.core.analysis.assemblyanalyzer/) -Core analyzer that reads a .NET assembly and extracts PE, metadata, IL, and string information. -Uses [PEReader](https://learn.microsoft.com/dotnet/api/system.reflection.portableexecutable.pereader) and [MetadataReader](https://learn.microsoft.com/dotnet/api/system.reflection.metadata.metadatareader) from the BCL. +Core analyzer that reads .NET assemblies, Webcil app assemblies, native binaries, and raw Wasm +modules. It uses BCL metadata/PE readers where possible and routes runtime-native formats +through dotsider's format readers for IL, strings, symbols, disassembly, and size data. ```csharp public sealed class AssemblyAnalyzer : IDisposable @@ -46,7 +47,7 @@ public static class AssemblyIdentityFormat ### [AssemblyLoader](/api/dotsider.core.analysis.assemblyloader/) Shared factory for opening assembly files. Handles apphosts (companion .dll redirect), -single-file bundles (entry assembly extraction), Native AOT binaries, and direct +single-file bundles (entry assembly extraction), Native AOT binaries, raw Wasm modules, and direct .dll/.exe loading. Returns an [AssemblyOpenResult](/api/dotsider.core.analysis.models.assemblyopenresult/) that preserves the distinction so callers can decide how to present each case (e.g. showing an apphost dialog). diff --git a/docs/src/content/docs/api/Dotsider-Core-Protocol-WasmPayloadBuilder.md b/docs/src/content/docs/api/Dotsider-Core-Protocol-WasmPayloadBuilder.md new file mode 100644 index 00000000..f4d082a2 --- /dev/null +++ b/docs/src/content/docs/api/Dotsider-Core-Protocol-WasmPayloadBuilder.md @@ -0,0 +1,80 @@ +--- +title: "WasmPayloadBuilder" +description: "Builds JSON-ready WebAssembly payloads shared by direct MCP tools, the CLI, and the diagnostics session protocol. The payloads describe raw SDK-produced Wasm modules such as dotnet.native.wasm, not ECMA-335 metadata assemblies." +slug: api/dotsider.core.protocol.wasmpayloadbuilder +sidebar: + order: 4 +--- + +**Namespace:** `Dotsider.Core.Protocol` + +**Assembly:** Dotsider.Core.dll + +Builds JSON-ready WebAssembly payloads shared by direct MCP tools, the CLI, and the +diagnostics session protocol. The payloads describe raw SDK-produced Wasm modules such as +`dotnet.native.wasm`, not ECMA-335 metadata assemblies. + +```csharp +public static class WasmPayloadBuilder +``` + +## Inheritance + +[Object](https://learn.microsoft.com/dotnet/api/system.object) → **WasmPayloadBuilder** + +## Methods + +### BuildFunctions(AssemblyAnalyzer) + +Builds a function inventory for a WebAssembly module. Imported functions and file-backed +defined functions share the same Wasm function-index space, matching direct call operands +and symbol-map entries. + +**Parameters:** + +- `analyzer` ([AssemblyAnalyzer](/api/dotsider.core.analysis.assemblyanalyzer/)): The analyzer that opened a raw Wasm module. + +**Returns:** [Object](https://learn.microsoft.com/dotnet/api/system.object) + +A JSON-ready function inventory payload. + +```csharp +public static object BuildFunctions(AssemblyAnalyzer analyzer) +``` + +### BuildSections(AssemblyAnalyzer) + +Builds a section-table payload for a WebAssembly module. Each section keeps its raw id, +display name, file payload offset, and payload size so callers can jump to the bytes that +the SDK emitted. + +**Parameters:** + +- `analyzer` ([AssemblyAnalyzer](/api/dotsider.core.analysis.assemblyanalyzer/)): The analyzer that opened a raw Wasm module. + +**Returns:** [Object](https://learn.microsoft.com/dotnet/api/system.object) + +A JSON-ready section table payload. + +```csharp +public static object BuildSections(AssemblyAnalyzer analyzer) +``` + +### BuildSummary(AssemblyAnalyzer) + +Builds a compact WebAssembly module summary for protocol surfaces. Returns null when the +analyzer is not a raw Wasm module so callers can include it unconditionally beside other +binary-kind summaries. + +**Parameters:** + +- `analyzer` ([AssemblyAnalyzer](/api/dotsider.core.analysis.assemblyanalyzer/)): The analyzer whose raw Wasm summary should be serialized. + +**Returns:** [Object](https://learn.microsoft.com/dotnet/api/system.object) + +A JSON-ready summary object, or null when the analyzer is not raw Wasm. + +```csharp +public static object? BuildSummary(AssemblyAnalyzer analyzer) +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Protocol-WebcilPayloadBuilder.md b/docs/src/content/docs/api/Dotsider-Core-Protocol-WebcilPayloadBuilder.md new file mode 100644 index 00000000..7f283df1 --- /dev/null +++ b/docs/src/content/docs/api/Dotsider-Core-Protocol-WebcilPayloadBuilder.md @@ -0,0 +1,43 @@ +--- +title: "WebcilPayloadBuilder" +description: "Builds JSON-ready Webcil payloads shared by CLI, MCP, and diagnostics session output. Webcil is a managed assembly container used in browser-wasm publishes, so the payload is provenance beside the normal metadata/IL facts rather than a separate native module view." +slug: api/dotsider.core.protocol.webcilpayloadbuilder +sidebar: + order: 4 +--- + +**Namespace:** `Dotsider.Core.Protocol` + +**Assembly:** Dotsider.Core.dll + +Builds JSON-ready Webcil payloads shared by CLI, MCP, and diagnostics session output. +Webcil is a managed assembly container used in browser-wasm publishes, so the payload is +provenance beside the normal metadata/IL facts rather than a separate native module view. + +```csharp +public static class WebcilPayloadBuilder +``` + +## Inheritance + +[Object](https://learn.microsoft.com/dotnet/api/system.object) → **WebcilPayloadBuilder** + +## Methods + +### BuildSummary(AssemblyAnalyzer) + +Builds a compact Webcil summary for protocol surfaces. Returns null when the analyzer did +not open a Webcil assembly, allowing callers to include the property unconditionally. + +**Parameters:** + +- `analyzer` ([AssemblyAnalyzer](/api/dotsider.core.analysis.assemblyanalyzer/)): The analyzer whose Webcil provenance should be serialized. + +**Returns:** [Object](https://learn.microsoft.com/dotnet/api/system.object) + +A JSON-ready Webcil summary object, or null when the analyzer is not Webcil. + +```csharp +public static object? BuildSummary(AssemblyAnalyzer analyzer) +``` + diff --git a/docs/src/content/docs/api/Dotsider-Core-Protocol.md b/docs/src/content/docs/api/Dotsider-Core-Protocol.md index 1c7ba647..9cdb3795 100644 --- a/docs/src/content/docs/api/Dotsider-Core-Protocol.md +++ b/docs/src/content/docs/api/Dotsider-Core-Protocol.md @@ -77,3 +77,23 @@ both call these, so the two transports cannot drift apart in shape or semantics. public static class SizeDiffPayloadBuilder ``` +### [WasmPayloadBuilder](/api/dotsider.core.protocol.wasmpayloadbuilder/) + +Builds JSON-ready WebAssembly payloads shared by direct MCP tools, the CLI, and the +diagnostics session protocol. The payloads describe raw SDK-produced Wasm modules such as +`dotnet.native.wasm`, not ECMA-335 metadata assemblies. + +```csharp +public static class WasmPayloadBuilder +``` + +### [WebcilPayloadBuilder](/api/dotsider.core.protocol.webcilpayloadbuilder/) + +Builds JSON-ready Webcil payloads shared by CLI, MCP, and diagnostics session output. +Webcil is a managed assembly container used in browser-wasm publishes, so the payload is +provenance beside the normal metadata/IL facts rather than a separate native module view. + +```csharp +public static class WebcilPayloadBuilder +``` + diff --git a/docs/src/content/docs/reference/cli.md b/docs/src/content/docs/reference/cli.md index 47d8712f..766e0de6 100644 --- a/docs/src/content/docs/reference/cli.md +++ b/docs/src/content/docs/reference/cli.md @@ -6,7 +6,7 @@ description: Complete command reference for the dotsider CLI. ## Synopsis ``` -dotsider # TUI — interactive assembly explorer +dotsider # TUI — interactive assembly/native module explorer dotsider # TUI — browse NuGet package contents dotsider diff # TUI — assembly comparison, or AOT size diff for mstat inputs @@ -49,6 +49,10 @@ dotsider analyze MyAotApp.exe --why System.Text.Json.JsonSerializer # AOT depen dotsider analyze MyAotApp.exe --symbols # native symbols with addresses, sizes, and file:line dotsider analyze MyAotApp.exe --disasm 'Program.
$' # disassemble a native function by name dotsider analyze MyAotApp.exe --disasm 0x140001300 # disassemble a native function by address +dotsider analyze dotnet.native.wasm --symbols # SDK Wasm function symbols +dotsider analyze dotnet.native.wasm --disasm 0x1234 # Wasm function body by file/code offset +dotsider analyze dotnet.native.wasm --disasm func:42 # Wasm function body by function index +dotsider analyze WasmConsole.wasm --il WasmCalculator.Add # Webcil app assembly → managed IL ``` If the file is a native apphost with a companion `.dll`, `analyze` auto-redirects. If it's a self-contained single-file bundle, `analyze` extracts the entry assembly from the bundle. Both cases print a note to stderr. @@ -63,6 +67,8 @@ When portable PDB data is available, default output reports where it came from, A ReadyToRun (crossgen2) image keeps its full metadata and adds precompiled native bodies. The default output reports a `ReadyToRun` line (version, status, architecture, composite/component). `--r2r-correlate` with no argument prints the precompiled-method stats; with a `Type.Method`, a `0x06…` token, or a `0x…` native address it prints the method's IL beside its native code ranges, resolving call targets through the import tables. An overloaded name lists the candidates and exits non-zero. A composite `*.r2r.dll` resolves its component assemblies by name and MVID from the siblings beside it; a component DLL routes its native code to the owner composite. `--json` carries the structured per-range IL and native arrays. +Raw `dotnet.native.wasm` modules from a browser-wasm publish open directly. Default output reports a `WebAssembly (.NET)` block with section, type/table/memory/global, function, code, data, import/export, and symbol-map counts. `--symbols` lists file-backed Wasm functions named from `dotnet.native.js.symbols`, the Wasm name section, exports, or `func_N` fallbacks. `--disasm <0xoffset-or-name-or-func:index>` decodes a Wasm32 function body, resolves direct calls through the module's function index, and annotates locals, globals, tables, and indirect-call types from the parsed standard sections. Webcil app assemblies such as `WasmConsole.wasm` are different: dotsider unwraps their managed metadata, reports a `Webcil` line, and uses the normal `--types`, `--methods`, `--il`, PDB, and Source Link paths. + | Option | Description | |--------|-------------| | `--types` | List type definitions | @@ -74,8 +80,8 @@ A ReadyToRun (crossgen2) image keeps its full metadata and adds precompiled nati | `-n`, `--min-len ` | Minimum length for raw string extraction (default: 4) | | `--fields` | List field definitions | | `--size` | Show size breakdown | -| `--symbols` | List native symbols with provenance (Native AOT and other native binaries) | -| `--disasm ` | Disassemble a native function to assembly (Native AOT and other native binaries) | +| `--symbols` | List native symbols with provenance (Native AOT, ReadyToRun, Wasm, and native binaries) | +| `--disasm ` | Disassemble a native or Wasm function | | `--why ` | Explain why a type or method is in a Native AOT binary | | `--r2r-correlate [name-or-0xVA]` | ReadyToRun stats, or a method's IL beside its precompiled native code | | `--bundle` | Show single-file bundle manifest | diff --git a/docs/src/content/docs/reference/mcp.md b/docs/src/content/docs/reference/mcp.md index 32d86052..f6df0263 100644 --- a/docs/src/content/docs/reference/mcp.md +++ b/docs/src/content/docs/reference/mcp.md @@ -74,15 +74,19 @@ Add to your MCP client configuration (e.g. `.mcp.json` for Claude Code): Tools work in two modes: -- **Direct mode** — pass an assembly path, get results (no TUI needed) +- **Direct mode** — pass an assembly or supported native module path, get results (no TUI needed) - **Session mode** — connect to a running dotsider TUI via Unix domain socket for live state, tracing, and navigation Single-file executables and native apphosts are handled transparently in direct mode — the server extracts the entry assembly from bundles and redirects apphosts to their companion DLLs, matching CLI and TUI behavior. Portable PDB data is exposed when present, including provenance, Source Link mappings, sequence points, and local names. -Native AOT executables are recognized by their embedded ReadyToRun header: `get_assembly_info` reports `binaryKind` (`managed`, `nativeAot`, `readyToRun`, or `native`), a `nativeAotInfo` object with the RTR format version, section count, and heuristically recovered runtime version, plus `readyToRunSectionCount`, `recoveredTypeCount`, and `frozenStringCount`. `list_types` falls back to the types recovered from the embedded NativeFormat metadata, so it names a stripped binary's own types. `extract_strings` returns `rawStrings` (ASCII), `rawUtf16Strings`, and `frozenStrings` alongside the metadata heaps — for AOT binaries the raw scans and frozen literals are the populated categories, and the frozen strings are the AOT counterpart of the #US heap. +Native AOT executables are recognized by their embedded ReadyToRun header: `get_assembly_info` reports `binaryKind` (`managed`, `nativeAot`, `readyToRun`, `wasm`, or `native`), a `nativeAotInfo` object with the RTR format version, section count, and heuristically recovered runtime version, plus `readyToRunSectionCount`, `recoveredTypeCount`, and `frozenStringCount`. `list_types` falls back to the types recovered from the embedded NativeFormat metadata, so it names a stripped binary's own types. `extract_strings` returns `rawStrings` (ASCII), `rawUtf16Strings`, and `frozenStrings` alongside the metadata heaps — for AOT binaries the raw scans and frozen literals are the populated categories, and the frozen strings are the AOT counterpart of the #US heap. ReadyToRun (crossgen2) images keep their full metadata: `get_assembly_info` reports `binaryKind` `readyToRun` and a `readyToRun` object (version, status, architecture, composite/component, method counts). `correlate_r2r_method` takes a `Type.Method`, a `0x06…` token, or a `0x…` native address and returns the method's IL beside its precompiled native code ranges with import-resolved call targets; an overloaded name raises an error listing the candidates. It follows a composite across its component assemblies in both directions, resolving them by name and MVID. +Raw `dotnet.native.wasm` modules report `binaryKind` `wasm` and a `wasm` object from `get_assembly_info` with section, type/table/memory/global, function, code, data, import/export, and symbol-map counts. `get_native_symbols` lists defined Wasm functions and `get_native_disassembly` decodes Wasm32 bodies by name, `func:N`, decimal function index, or `0x…` file/code offset, with direct-call target names from the module function index plus local/global/table/type annotations from the parsed standard sections. Webcil app assemblies such as `WasmConsole.wasm` report `binaryKind` `managed` with a `webcil` object; metadata, IL, PDB, Source Link, and member-search tools work against the unwrapped managed assembly. + +For deeper raw Wasm inspection, `list_wasm_sections` returns the section table with payload offsets and sizes, and `list_wasm_functions` returns imported plus defined functions in the same function-index order used by Wasm `call` operands, symbol maps, and `get_native_disassembly`. + Native disassembly tools return structured instructions for x64, Arm64, x86, Arm32/Thumb-2, RISC-V64, LoongArch64, and Wasm32. For ReadyToRun, `get_native_disassembly` accepts a method name or address and renders all of that method's native ranges together. `diff_size` compares two Native AOT builds via their mstat size reports (bare `.mstat` files or binaries with sidecars): summary, per-assembly and per-namespace deltas, top contributors, and — only on request — the delta tree, pruned to a node cap with explicit truncation metadata. `check_size_budgets` evaluates size budgets against a build (optionally versus a baseline) and returns the per-budget report; budgets arrive as grammar strings, an inline budgets JSON document, or a budgets file path, at full parity with the CLI including named budgets and warning severity. See [Size Regression](/usage/size-regression/). diff --git a/docs/src/content/docs/usage/general.md b/docs/src/content/docs/usage/general.md index 58601bbe..3f55fd96 100644 --- a/docs/src/content/docs/usage/general.md +++ b/docs/src/content/docs/usage/general.md @@ -30,6 +30,12 @@ Opening a Native AOT executable adds a block below the standard fields. ILC stri A ReadyToRun (crossgen2) image keeps its full metadata and adds precompiled native bodies, so the standard fields stay and a **ReadyToRun** block is added, reporting the format version and status (`Valid`, `Corrupt`, or `UnsupportedVersion` — a broken header is surfaced, never hidden), the architecture, whether the image is a composite or a component (and its owner), and how many methods are precompiled. Inspect a method's native code in tab 3's **IL + Native** view or via `--r2r-correlate`. +## WebAssembly modules + +Opening `dotnet.native.wasm` from a browser-wasm publish adds a **WebAssembly (.NET)** block. The module has no ECMA-335 metadata of its own, so the dependency table is empty, but the General tab reports the Wasm version, section count, type/table/memory/global counts, element and data counts, the start function when present, defined/imported functions, code and data sizes, import/export counts, and whether `dotnet.native.js.symbols` was loaded for function names. Function symbols and disassembly live in tab 3 and the PE/Metadata **Symbols** sub-tab. + +Opening a Webcil app assembly from the same publish, for example `_framework/MyApp.wasm`, keeps the normal managed assembly view. The General tab adds a compact **Managed Webcil (.NET)** block showing the Webcil version, whether the payload was Wasm-wrapped, section count, and metadata size, while dependencies, metadata tables, IL, PDBs, and Source Link behave like any other managed assembly. + ## Text selection and copy The Assembly Info panel is a read-only editor. Click into it or press `Tab` to move focus there, then select text with click-drag or `Shift` + arrow keys. Press `y` to yank the selection to the clipboard. diff --git a/docs/src/content/docs/usage/hex-dump.md b/docs/src/content/docs/usage/hex-dump.md index 8854fb6e..9620781d 100644 --- a/docs/src/content/docs/usage/hex-dump.md +++ b/docs/src/content/docs/usage/hex-dump.md @@ -32,3 +32,5 @@ Press `Esc` to return to normal mode. ## Saving Press `Ctrl+S` in normal mode when bytes have been modified. The editor validates the PE image before writing — invalid edits are rejected. + +Raw `dotnet.native.wasm` modules and Webcil app assemblies open read-only in the Hex Dump. `x` from the Disassembly tab jumps to a raw Wasm function body; `x` from managed Webcil IL jumps to the Webcil-contained method body bytes. diff --git a/docs/src/content/docs/usage/il-inspector.md b/docs/src/content/docs/usage/il-inspector.md index 8f41f8a5..74d3e665 100644 --- a/docs/src/content/docs/usage/il-inspector.md +++ b/docs/src/content/docs/usage/il-inspector.md @@ -42,11 +42,13 @@ Select text in the disassembly pane with click-drag or `Shift` + arrow keys, the Press `/` to search method names or IL content. Matches are highlighted in both the tree and disassembly panes. -## Native mode (Native AOT) +## Native mode (Native AOT and WebAssembly) -Open a Native AOT binary and tab `3` is labeled **Disassembly** in native mode: the left tree lists the recovered **functions** bucketed namespace → type → function (managed-named functions parsed from the symbols), plus `(runtime)`, `(stubs)`, and `(functions)` buckets for the rest. Selecting a function disassembles it to real native code for x64, Arm64, x86, Arm32/Thumb-2, RISC-V64, LoongArch64, or Wasm32 on the right, with the same subtle syntax highlighting as the IL pane — address, mnemonic, registers, immediates, and the resolved target comment. +Open a Native AOT binary or a raw `dotnet.native.wasm` module and tab `3` is labeled **Disassembly** in native mode. Native AOT shows recovered **functions** bucketed namespace → type → function where symbols can be joined to managed names, plus `(runtime)`, `(stubs)`, and `(functions)` buckets for the rest. A Wasm module shows SDK function symbols from `dotnet.native.js.symbols`, the Wasm name section, exports, or synthetic `func_N` names. Selecting a function disassembles it to real native code for x64, Arm64, x86, Arm32/Thumb-2, RISC-V64, LoongArch64, or Wasm32 on the right, with the same subtle syntax highlighting as the IL pane — address, mnemonic, registers, immediates, and the resolved target comment. -Call and branch targets are resolved to names: a direct call shows `call Foo`, a target landing inside a function shows `Foo+0x12`, an intra-function jump becomes a synthesized `loc_…` label, a RIP-relative load names the referenced data symbol, and an indirect call through the import table resolves to `MODULE!Function`. Where the debug sidecar carries line data, `// file:line` annotations mark the source. +Call and branch targets are resolved to names: a direct call shows `call Foo`, a target landing inside a function shows `Foo+0x12`, an intra-function jump becomes a synthesized `loc_…` label, a RIP-relative load names the referenced data symbol, and an indirect call through the import table resolves to `MODULE!Function`. Wasm direct calls resolve by function index to imported or defined function names; indirect calls name their type/table operands, and local/global/table operands are annotated from the parsed standard sections when the module records enough type information. Where the debug sidecar carries line data, `// file:line` annotations mark the source. + +Webcil app assemblies such as `_framework/MyApp.wasm` are not shown in native mode. They unwrap to managed metadata, so tab `3` remains **IL Inspector** and the tree, IL, PDB, locals, Source Link, and go-to-definition behavior match a normal managed DLL. `Enter` on a resolved call/branch jumps to that function (the target is underlined to signal it's navigable); `Esc` returns. `x` jumps to the function's bytes in the Hex Dump. The Size Map and the PE/Metadata **Symbols** sub-tab cross-navigate into the native listing. diff --git a/docs/src/content/docs/usage/pe-metadata.md b/docs/src/content/docs/usage/pe-metadata.md index 32014ab2..20ca2890 100644 --- a/docs/src/content/docs/usage/pe-metadata.md +++ b/docs/src/content/docs/usage/pe-metadata.md @@ -5,11 +5,11 @@ description: COFF headers, CLR header, sections, and metadata tables. ![PE / Metadata tab](../../../assets/screenshots/pe-metadata.png) -The **PE / Metadata** tab (`2`) exposes the raw structure of the Portable Executable: +The **PE / Metadata** tab (`2`) exposes the raw structure of the loaded file. PE files and Webcil-managed `.wasm` assemblies show PE headers plus ECMA-335 metadata. Raw `dotnet.native.wasm` modules use the same tab strip for WebAssembly tables instead of showing misleading PE rows. - **COFF header** — machine type, number of sections, timestamp - **CLR header** — runtime version, flags, entry point token -- **Sections** — .text, .rsrc, .reloc with virtual addresses and sizes +- **Sections** — .text, .rsrc, .reloc for PE files, reconstructed Webcil sections for managed `.wasm` assemblies, or standard/custom Wasm sections for raw `dotnet.native.wasm` modules - **TypeDefs** — every type defined in the assembly - **MethodDefs** — every method with its RVA and flags - **AssemblyRefs** — referenced assembly metadata @@ -21,13 +21,17 @@ The **PE / Metadata** tab (`2`) exposes the raw structure of the Portable Execut - **Load Config** — the load configuration directory: security cookie, SEH handler count, and decoded Control Flow Guard flags - **R2R Sections** — the ReadyToRun section table of a Native AOT or crossgen2 ReadyToRun image, each region's id, virtual address, size, and file offset - **AOT Types** — types recovered from a Native AOT binary's embedded metadata; press Enter to see a type's methods -- **Symbols** — native symbols with addresses, sizes, and kinds, demangled to managed names where the binary's own metadata allows; press Enter for the mangled name, aliases, section, and source location +- **Symbols** — native symbols with addresses, sizes, and kinds, demangled or named from the binary's own metadata/symbol sidecars where available; press Enter for the mangled name, aliases, section, and source location -Imports and Exports need no CLR header, so they light up for native apphosts and Native AOT executables where the metadata tables are empty. They read whichever native format the binary uses: PE import descriptors on Windows, ELF needed libraries and versioned dynamic symbols on Linux, and Mach-O loaded dylibs and two-level-namespace bindings on macOS. Load Config is a PE-only structure and stays empty on ELF and Mach-O. +Imports and Exports need no CLR header, so they light up for native apphosts, Native AOT executables, and raw Wasm modules where the metadata tables are empty. They read whichever native format the binary uses: PE import descriptors on Windows, ELF needed libraries and versioned dynamic symbols on Linux, Mach-O loaded dylibs and two-level-namespace bindings on macOS, and Wasm import/export sections. + +For raw Wasm, the sub-tabs are relabeled to describe the module's own structure: Sections, Types, Functions, Tables, Memories, Globals, Data, Custom, Imports, Exports, Elements, Tags, Module, and Symbols. The section view includes standard sections such as type, table, memory, global, element, code, data-count, data, tag, and custom sections with exact payload offsets and sizes. + +Webcil app assemblies are managed assemblies stored in a WebAssembly-compatible container. dotsider unwraps their metadata and debug directory, so TypeDefs, MethodDefs, resources, IL, embedded portable PDBs, sidecar PDBs, and Source Link behave like a managed DLL even though the file extension is `.wasm`. R2R Sections and AOT Types apply to Native AOT binaries. ILC strips ECMA-335 metadata, but every AOT image embeds a ReadyToRun header that locates its runtime regions, and the reflection and stack-trace metadata it keeps still names the binary's own types and methods — so a stripped binary describes itself. Both work on every platform where the data is file-backed. For a crossgen2 ReadyToRun image the R2R Sections tab lists its classic section table instead (RuntimeFunctions, MethodDefEntryPoints, ImportSections, and the composite tables), each with its id, virtual address, size, and file offset. -Symbols reads whichever artifact the platform's publish produced — a native PDB on Windows, a `.dbg` ELF sidecar on Linux, or a dSYM bundle on macOS — after validating it against the binary's identity (PDB GUID and age, GNU build id / debuglink CRC, or Mach-O UUID; a mismatching file is rejected, never misread). ILC's mangled names are demangled by joining against the binary's own recovered metadata, so a managed name is marked exact only when the join is unambiguous. ReadyToRun symbols come from the runtime-function map and carry the owning MethodDef token. Functions carry their declaring source file and line when the symbol file records them. Without a symbol file, unwind data (`.pdata`, `.eh_frame`, or `LC_FUNCTION_STARTS`) still recovers nameless function boundaries — enough for counts and size histograms, though unwind data can miss leaf and thunk functions. +Symbols reads whichever artifact the platform's publish produced — a native PDB on Windows, a `.dbg` ELF sidecar on Linux, a dSYM bundle on macOS, or `dotnet.native.js.symbols` beside `dotnet.native.wasm` — after validating or parsing the format-specific identity. ILC's mangled names are demangled by joining against the binary's own recovered metadata, so a managed name is marked exact only when the join is unambiguous. ReadyToRun symbols come from the runtime-function map and carry the owning MethodDef token. Wasm symbols come from the Wasm function/code sections, with names layered from the SDK symbol map, the Wasm name section, exports, then synthetic `func_N` fallbacks. Functions carry their declaring source file and line when the symbol file records them. Without a symbol file, unwind data (`.pdata`, `.eh_frame`, or `LC_FUNCTION_STARTS`) still recovers nameless function boundaries for native PE/ELF/Mach-O binaries — enough for counts and size histograms, though unwind data can miss leaf and thunk functions. ## Text selection and copy diff --git a/docs/src/content/docs/usage/size-map.md b/docs/src/content/docs/usage/size-map.md index 213c670a..c3c90647 100644 --- a/docs/src/content/docs/usage/size-map.md +++ b/docs/src/content/docs/usage/size-map.md @@ -41,6 +41,12 @@ With no symbol file either, unwind data still yields nameless function boundarie For a ReadyToRun (crossgen2) image the treemap sizes the **precompiled native code** — each method's hot, funclet, and cold code ranges summed and grouped under assembly > namespace > type — rather than IL bytes, so the map reflects what crossgen2 actually emitted. Composite images group by the resolved component assembly metadata. +## WebAssembly modules + +For a raw `dotnet.native.wasm` module the treemap sizes the Wasm payload itself. The top level splits into function bodies, data segments, and remaining sections, with code/data section overhead called out separately. This is a file-size view of the SDK-produced runtime module, not a managed assembly size view. + +For a Webcil app assembly such as `_framework/MyApp.wasm`, the treemap uses the managed IL view. The file is a WebAssembly-compatible container, but the code being analyzed is still ECMA-335 metadata and IL. + ## Comparing two builds To see where the bytes went *between* two AOT builds — the same treemap, but weighted by delta — see the size diff in [Diff Mode](/usage/diff-mode/) and the CI gate in [Size Regression](/usage/size-regression/). diff --git a/samples/README.md b/samples/README.md index c44f1dbb..4a63dfa2 100644 --- a/samples/README.md +++ b/samples/README.md @@ -17,7 +17,8 @@ Sample .NET projects used as test fixtures for dotsider's analysis, diff, and tr | [NativeAotConsoleV2](NativeAotConsoleV2/) | Console app (Native AOT) | Versioned Native AOT fixture paired with NativeAotConsole for size-diff regressions and dependency-chain reporting | | [NativeAotArtifactsConsole](NativeAotArtifactsConsole/) | Console app (Native AOT, artifacts output) | UseArtifactsOutput fixture proving obj-tree sidecar discovery without sibling-copied mstat/DGML files | | [NativeAotLibrary](NativeAotLibrary/) | Native AOT library | Native library output (`.dll`, `.so`, `.dylib`) with exported unmanaged entry points and tree/rsp sidecar probing | -| [ReadyToRunConsole](ReadyToRunConsole/) | Console app (ReadyToRun) | Non-composite crossgen2 fixture for method-to-native-body maps, cross-RID decoder coverage, apphost redirect, and browser-wasm runtime module coverage | +| [ReadyToRunConsole](ReadyToRunConsole/) | Console app (ReadyToRun) | Non-composite crossgen2 fixture for method-to-native-body maps, cross-RID decoder coverage, and apphost redirect | +| [WasmConsole](WasmConsole/) | Console app (browser-wasm) | Real SDK browser-wasm fixture that emits raw runtime `dotnet.native.wasm`, `dotnet.native.js.symbols`, the Webcil-managed app assembly `WasmConsole.wasm`, and an optional AOT-compiled browser-wasm variant | | [ReadyToRunComposite](ReadyToRunComposite/) | Console app (composite ReadyToRun) | Composite crossgen2 image that resolves component assemblies by name and MVID in both directions | | [ReadyToRunComponentLib](ReadyToRunComponentLib/) | Library (composite ReadyToRun component) | Component metadata fixture used by the composite image and owner-composite resolution tests | | [Dotted.Name.App](Dotted.Name.App/) | Console app | Dotted assembly name for cross-platform apphost detection testing | @@ -40,5 +41,5 @@ Sample .NET projects used as test fixtures for dotsider's analysis, diff, and tr | [NetFxBindingRedirects.Clr2.CulturedLib](NetFxBindingRedirects.Clr2.CulturedLib/) | Library (.NET Fx 3.5) | Neutral + French satellite for CLR 2 culture-aware probing. Satellite is built via the v3.5 framework `csc.exe` (the SDK's `al.exe` only emits CLR4 metadata); the build target skips on hosts without the legacy framework toolchain | All managed samples target .NET 10 with nullable reference types enabled unless noted otherwise. -Native-code fixture publishes are intentionally tolerant. The shared test fixture attempts the current RID, `win-x86`, `linux-arm`, `linux-riscv64`, `linux-loongarch64`, and `browser-wasm` where relevant; missing runtime packs or workloads leave those paths null and the dependent tests skip with a targeted reason. The committed disassembly oracle fixtures under `tests/Dotsider.Tests/Fixtures/Disasm` cover architectures that the public SDK cannot always publish locally. +Native-code fixture publishes are intentionally tolerant. The shared test fixture attempts the current RID, `win-x86`, `linux-arm`, `linux-riscv64`, `linux-loongarch64`, and `browser-wasm` where relevant; missing runtime packs or workloads leave those paths null and the dependent tests skip with a targeted reason. The browser-wasm publish contributes the real SDK runtime module (`dotnet.native.wasm`), symbol map (`dotnet.native.js.symbols`), and Webcil-managed app assembly (`WasmConsole.wasm`) used by raw Wasm and Webcil open/disassembly tests. When the `wasm-tools` workload and SDK support AOT compilation, the same sample also produces an AOT browser-wasm module so decoder coverage exercises the runtime's AOT-shaped Wasm output rather than only the interpreter module. The committed disassembly oracle fixtures under `tests/Dotsider.Tests/Fixtures/Disasm` cover architectures that the public SDK cannot always publish locally. The `NetFxBindingRedirects*` projects build only on Windows: the six original projects target `net48` (CLR 4); the eight `NetFxBindingRedirects.Clr2*` siblings target `net35` (CLR 2). Both cohorts compile against the `Microsoft.NETFramework.ReferenceAssemblies` package so neither requires a Windows SDK install; the CLR 2 satellite-build step further requires .NET Framework 3.5 to be enabled on the build host (skipped cleanly otherwise). diff --git a/samples/WasmConsole/Program.cs b/samples/WasmConsole/Program.cs new file mode 100644 index 00000000..5822a241 --- /dev/null +++ b/samples/WasmConsole/Program.cs @@ -0,0 +1,22 @@ +// A browser-wasm fixture for testing dotsider's raw WebAssembly and Webcil support. +// The publish emits dotnet.native.wasm, dotnet.native.js.symbols, and Webcil-wrapped +// managed assembly modules under AppBundle/_framework. +using System.Runtime.CompilerServices; + +Console.WriteLine("Hello from Wasm!"); + +var calculator = new WasmCalculator(3); +Console.WriteLine(calculator.Add(4)); +Console.WriteLine(calculator.Describe("dotsider")); + +/// A tiny type whose methods stay visible in the Webcil metadata fixture. +internal sealed class WasmCalculator(int seed) +{ + /// Adds the seed to the supplied value. + [MethodImpl(MethodImplOptions.NoInlining)] + public int Add(int value) => seed + value; + + /// Formats a string through ordinary managed IL. + [MethodImpl(MethodImplOptions.NoInlining)] + public string Describe(string name) => $"{name}:{seed}"; +} diff --git a/samples/WasmConsole/WasmConsole.csproj b/samples/WasmConsole/WasmConsole.csproj new file mode 100644 index 00000000..76657d23 --- /dev/null +++ b/samples/WasmConsole/WasmConsole.csproj @@ -0,0 +1,12 @@ + + + + Exe + net10.0 + enable + enable + true + true + true + + diff --git a/src/Dotsider.Core/Analysis/AssemblyAnalyzer.cs b/src/Dotsider.Core/Analysis/AssemblyAnalyzer.cs index 65866526..a0b24037 100644 --- a/src/Dotsider.Core/Analysis/AssemblyAnalyzer.cs +++ b/src/Dotsider.Core/Analysis/AssemblyAnalyzer.cs @@ -10,14 +10,16 @@ namespace Dotsider.Core.Analysis; /// -/// Core analyzer that reads a .NET assembly and extracts PE, metadata, IL, and string information. -/// Uses and from the BCL. +/// Core analyzer that reads .NET assemblies, Webcil app assemblies, native binaries, and raw Wasm +/// modules. It uses BCL metadata/PE readers where possible and routes runtime-native formats +/// through dotsider's format readers for IL, strings, symbols, disassembly, and size data. /// public sealed class AssemblyAnalyzer : IDisposable { private readonly Stream _stream; - private readonly PEReader? _peReader; - private readonly MetadataReader? _metadataReader; + private PEReader? _peReader; + private MetadataReader? _metadataReader; + private MetadataReaderProvider? _metadataReaderProvider; private MetadataReaderProvider? _pdbReaderProvider; private MetadataReader? _pdbReader; private readonly byte[] _rawBytes; @@ -59,6 +61,10 @@ public sealed class AssemblyAnalyzer : IDisposable private readonly System.Threading.Lock _readyToRunModelLock = new(); private ReadyToRunIndex? _readyToRunIndex; private bool _readyToRunIndexProbed; + private Wasm.WebcilImageReader? _webcilReader; + private WebcilInfo? _webcilInfo; + private WasmModuleInfo? _wasmModuleInfo; + private bool _wasmModuleProbed; private PreIlcSidecars? _preIlcSidecars; private bool _preIlcProbed; private PreIlcCompanionSet? _preIlcCompanions; @@ -89,6 +95,9 @@ public AssemblyAnalyzer(string filePath) IsReadOnly = fileInfo.IsReadOnly; _stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + if (TryInitializeWebcil(_rawBytes)) + return; + try { _peReader = new PEReader(_stream); @@ -107,7 +116,7 @@ public AssemblyAnalyzer(string filePath) catch (BadImageFormatException) when (IsNativeExecutable(_rawBytes)) { // Non-PE native binary (ELF or Mach-O on Linux/macOS), such as a - // .NET apphost or NativeAOT output. Raw bytes are already loaded + // .NET apphost, NativeAOT output, or raw WebAssembly module. Raw bytes are already loaded // for hex dump; PE-specific analysis will be empty. _peReader?.Dispose(); _peReader = null; @@ -152,6 +161,9 @@ public AssemblyAnalyzer(byte[] bytes, string filePath, string? sourceBundlePath CreatedTime = DateTime.UtcNow; _stream = new MemoryStream(bytes, writable: false); + if (TryInitializeWebcil(_rawBytes)) + return; + try { _peReader = new PEReader(_stream); @@ -233,6 +245,12 @@ public AssemblyAnalyzer(byte[] bytes, string filePath, string? sourceBundlePath /// Whether the PE file contains .NET metadata. public bool HasMetadata => _metadataReader is not null; + /// + /// Parsed Webcil provenance when this analyzer opened a Webcil managed assembly directly or + /// unwrapped one from a WebAssembly container. Null for PE, raw Wasm, ELF, and Mach-O inputs. + /// + public WebcilInfo? WebcilInfo => _webcilInfo; + /// /// Facts from the embedded ReadyToRun header when this is a Native AOT binary, /// or null. Only probed for metadata-less files — a managed ReadyToRun assembly @@ -275,6 +293,28 @@ public ReadyToRunInfo? ReadyToRunInfo } } + /// + /// Parsed WebAssembly module facts when this file is a raw .wasm module, or null for + /// PE, ELF, and Mach-O inputs. The main .NET browser-wasm native module is + /// dotnet.native.wasm. + /// + public WasmModuleInfo? WasmModuleInfo + { + get + { + ObjectDisposedException.ThrowIf(_disposed, this); + if (!_wasmModuleProbed) + { + _wasmModuleInfo = _webcilReader is null && Wasm.WasmModuleReader.IsWasmModule(_rawBytes) + ? Wasm.WasmModuleReader.Read(_rawBytes, FilePath) + : null; + _wasmModuleProbed = true; + } + + return _wasmModuleInfo; + } + } + /// /// The precompiled methods of a ReadyToRun image joined to their native code ranges, or an /// empty list when this is not a usable ReadyToRun image. Built lazily from the entry-point @@ -334,13 +374,14 @@ public AssemblyAnalyzer ReadyToRunMetadataProviderFor(Guid mvid) => or ReadyToRunStatus.Corrupt or ReadyToRunStatus.UnsupportedVersion } ? BinaryKind.ReadyToRun : HasMetadata ? BinaryKind.Managed : NativeAotInfo is not null ? BinaryKind.NativeAot + : WasmModuleInfo is not null ? BinaryKind.Wasm : BinaryKind.Native; /// Whether this image carries ECMA-335 metadata (managed or ReadyToRun). public bool HasManagedMetadata => HasMetadata; /// Whether this image has precompiled native method bodies mapped to managed methods. - public bool HasEmbeddedNativeCode => BinaryKind is BinaryKind.ReadyToRun or BinaryKind.NativeAot; + public bool HasEmbeddedNativeCode => BinaryKind is BinaryKind.ReadyToRun or BinaryKind.NativeAot or BinaryKind.Wasm; /// Whether this is a crossgen2 ReadyToRun image. public bool IsReadyToRun => BinaryKind == BinaryKind.ReadyToRun; @@ -530,6 +571,8 @@ public NativeSymbolInfo? NativeSymbols ReadyToRunMethods, ReadyToRunInfo!.Architecture, mapUsable: ReadyToRunInfo.Status == ReadyToRunStatus.Valid && ReadyToRunMethods.Count > 0, diagnostic: ReadyToRunInfo.Diagnostic) + : WasmModuleInfo is { } wasm + ? Wasm.WasmSymbolBuilder.Build(wasm) : BinaryKind != BinaryKind.Managed ? NativeSymbolReader.Read(FilePath, _rawBytes, RecoveredTypes) : null; @@ -948,7 +991,9 @@ public IReadOnlyList Resources public MethodBodyBlock? GetMethodBody(MethodDefInfo method) { ObjectDisposedException.ThrowIf(_disposed, this); - if (method.Rva == 0 || _peReader is null) return null; + if (method.Rva == 0) return null; + if (_webcilReader is not null) return _webcilReader.GetMethodBody(method.Rva); + if (_peReader is null) return null; return _peReader.GetMethodBody(method.Rva); } @@ -1226,6 +1271,7 @@ public void Dispose() foreach (var sibling in owned) sibling.Dispose(); _pdbReaderProvider?.Dispose(); + _metadataReaderProvider?.Dispose(); _peReader?.Dispose(); _stream.Dispose(); } @@ -1239,6 +1285,9 @@ private static bool IsNativeExecutable(ReadOnlySpan bytes) { if (bytes.Length < 4) return false; + if (Wasm.WasmModuleReader.IsWasmModule(bytes)) + return true; + // ELF: \x7fELF if (bytes[0] == 0x7F && bytes[1] == 0x45 && bytes[2] == 0x4C && bytes[3] == 0x46) return true; @@ -1251,6 +1300,7 @@ private static bool IsNativeExecutable(ReadOnlySpan bytes) private IReadOnlyList ReadNativeImports() { if (_peReader is not null) return PeDirectoryReader.ReadImports(_peReader); + if (WasmModuleInfo is { } wasm) return ReadWasmImports(wasm); if (ElfImageReader.IsElf(_rawBytes)) return ElfImageReader.ReadImports(_rawBytes); if (MachOImageReader.IsMachO(_rawBytes)) return MachOImageReader.ReadImports(_rawBytes); return []; @@ -1259,11 +1309,35 @@ private IReadOnlyList ReadNativeImports() private IReadOnlyList ReadNativeExports() { if (_peReader is not null) return PeDirectoryReader.ReadExports(_peReader); + if (WasmModuleInfo is { } wasm) return ReadWasmExports(wasm); if (ElfImageReader.IsElf(_rawBytes)) return ElfImageReader.ReadExports(_rawBytes); if (MachOImageReader.IsMachO(_rawBytes)) return MachOImageReader.ReadExports(_rawBytes); return []; } + private static IReadOnlyList ReadWasmImports(WasmModuleInfo wasm) => + [ + .. wasm.Imports + .Where(static i => i.Kind == WasmExternalKind.Function) + .GroupBy(static i => i.ModuleName, StringComparer.Ordinal) + .OrderBy(static g => g.Key, StringComparer.Ordinal) + .Select(static g => new ImportedModuleInfo( + g.Key, + [.. g.Select(static i => new ImportedFunctionInfo(i.Name, Ordinal: null, Hint: null))])) + ]; + + private static IReadOnlyList ReadWasmExports(WasmModuleInfo wasm) => + [ + .. wasm.Exports + .Where(static e => e.Kind == WasmExternalKind.Function) + .OrderBy(static e => e.Name, StringComparer.Ordinal) + .Select(static e => new ExportedFunctionInfo( + Ordinal: e.Index, + Name: e.Name, + Rva: e.Index, + ForwardedTo: null)) + ]; + /// /// Reads the target architecture from an ELF or Mach-O header. The bytes have /// already passed . @@ -1272,6 +1346,9 @@ private static string GetNativeArchitecture(ReadOnlySpan bytes) { if (bytes.Length < 8) return "Unknown"; + if (Wasm.WasmModuleReader.IsWasmModule(bytes)) + return "Wasm32"; + // ELF: e_machine is a u16 at offset 18; EI_DATA at offset 5 selects endianness if (bytes[0] == 0x7F && bytes[1] == 0x45 && bytes[2] == 0x4C && bytes[3] == 0x46) { @@ -1307,6 +1384,24 @@ private static string GetNativeArchitecture(ReadOnlySpan bytes) }; } + private bool TryInitializeWebcil(ReadOnlySpan bytes) + { + if (!Wasm.WebcilImageReader.TryRead(bytes, out var webcil) || webcil is null) + return false; + + _peReader = null; + _webcilReader = webcil; + _webcilInfo = webcil.Info; + _metadataReaderProvider = webcil.CreateMetadataReaderProvider(); + _metadataReader = _metadataReaderProvider.GetMetadataReader(); + ClrHeader = webcil.ClrHeader; + Architecture = "Wasm32"; + ReadAssemblyIdentity(); + ReadTargetFramework(); + ReadDebugInformation(); + return true; + } + private void ReadAssemblyIdentity() { if (_metadataReader is null || !_metadataReader.IsAssembly) return; @@ -1405,6 +1500,14 @@ private void ReadClrHeader() private void ReadDebugInformation() { + if (_webcilReader is not null) + { + _debugDirectory = [.. _webcilReader.ReadDebugDirectory()]; + OpenPortablePdb(); + _sourceLink = PortablePdbUtilities.ReadSourceLink(_pdbReader); + return; + } + if (_peReader is null) { _debugDirectory = []; @@ -1479,6 +1582,12 @@ private string FormatEmbeddedPortablePdbPayload(DebugDirectoryEntry entry) private void OpenPortablePdb() { + if (_webcilReader is not null) + { + OpenWebcilPortablePdb(); + return; + } + if (_peReader is null) { PdbProvenance = new PdbProvenance(PdbProvenanceKind.NoDebugDirectory); @@ -1577,6 +1686,84 @@ private void OpenPortablePdb() } } + private void OpenWebcilPortablePdb() + { + if (_webcilReader is null) + return; + + if (_debugDirectory is not { Count: > 0 }) + { + PdbProvenance = new PdbProvenance(PdbProvenanceKind.NoDebugDirectory); + return; + } + + var embeddedEntry = _webcilReader.EmbeddedPortablePdbEntry(); + if (embeddedEntry is not null && TryOpenWebcilEmbeddedPortablePdb(embeddedEntry.Value)) + return; + + var codeViewEntry = _webcilReader.CodeViewEntry(); + if (codeViewEntry is null) + { + PdbProvenance = new PdbProvenance(PdbProvenanceKind.NoDebugDirectory); + return; + } + + Wasm.WebcilCodeViewData codeViewData; + try + { + codeViewData = _webcilReader.ReadCodeView(codeViewEntry.Value); + } + catch (Exception ex) when (ex is BadImageFormatException or IOException) + { + PdbProvenance = new PdbProvenance( + PdbProvenanceKind.UnsupportedWindowsPdb, + Details: $"UnsupportedWindowsPdb ({ex.Message})"); + return; + } + + var probePaths = GetSidecarProbePaths(codeViewData.Path); + var foundPath = probePaths.FirstOrDefault(File.Exists); + if (foundPath is null) + { + var expected = probePaths.Count > 0 ? probePaths[0] : codeViewData.Path; + PdbProvenance = new PdbProvenance( + PdbProvenanceKind.CodeViewSidecarMissing, + expected, + $"CodeViewSidecarMissing ({expected})"); + return; + } + + if (!TryOpenPortablePdbFile(foundPath, codeViewData.Guid, codeViewData.Age, out var mismatch)) + { + PdbProvenance = mismatch + ? new PdbProvenance( + PdbProvenanceKind.CodeViewSidecarMismatched, + foundPath, + $"CodeViewSidecarMismatched ({foundPath})") + : new PdbProvenance( + PdbProvenanceKind.UnsupportedWindowsPdb, + foundPath, + $"UnsupportedWindowsPdb ({foundPath})"); + } + } + + private bool TryOpenWebcilEmbeddedPortablePdb(Wasm.WebcilDebugEntry entry) + { + if (_webcilReader is null) return false; + + try + { + _pdbReaderProvider = _webcilReader.ReadEmbeddedPortablePdb(entry); + _pdbReader = _pdbReaderProvider.GetMetadataReader(); + PdbProvenance = new PdbProvenance(PdbProvenanceKind.Embedded); + return true; + } + catch (Exception ex) when (ex is ArgumentException or BadImageFormatException or InvalidOperationException) + { + return false; + } + } + /// /// Looks for a Windows native PDB beside the binary whose GUID and age match the CodeView /// entry, using the cheap block-level probe. Probes the binary's own directory only — like @@ -1776,7 +1963,21 @@ private IReadOnlyList ReadLocalSlots(MethodDefinitionHandle metho private List ReadSections() { - if (_peReader is null) return []; + if (_webcilReader is not null) + return [.. _webcilReader.ReadSections()]; + + if (_peReader is null) + { + if (WasmModuleInfo is not { } wasm) return []; + return [.. wasm.Sections.Select(s => new SectionInfo( + Name: s.Name, + VirtualAddress: checked((int)s.FileOffset), + VirtualSize: s.Size, + RawDataOffset: checked((int)s.FileOffset), + RawDataSize: s.Size, + Characteristics: 0))]; + } + return [.. _peReader.PEHeaders.SectionHeaders .Select(s => new SectionInfo( Name: s.Name, @@ -2040,14 +2241,22 @@ private List ReadResources() try { var resourcesRva = ClrHeader.ResourcesRva; - var sectionData = _peReader!.GetSectionData(resourcesRva); - if (sectionData.Length > 0) + if (_webcilReader is not null) + { + if (_webcilReader.TryReadInt32AtRva(resourcesRva, offset, out var webcilSize)) + size = webcilSize; + } + else if (_peReader is not null) { - var reader = sectionData.GetReader(); - reader.Offset += offset; - if (reader.RemainingBytes >= 4) + var sectionData = _peReader.GetSectionData(resourcesRva); + if (sectionData.Length > 0) { - size = reader.ReadInt32(); + var reader = sectionData.GetReader(); + reader.Offset += offset; + if (reader.RemainingBytes >= 4) + { + size = reader.ReadInt32(); + } } } } diff --git a/src/Dotsider.Core/Analysis/AssemblyLoader.cs b/src/Dotsider.Core/Analysis/AssemblyLoader.cs index 00776b75..ae147e03 100644 --- a/src/Dotsider.Core/Analysis/AssemblyLoader.cs +++ b/src/Dotsider.Core/Analysis/AssemblyLoader.cs @@ -4,7 +4,7 @@ namespace Dotsider.Core.Analysis; /// /// Shared factory for opening assembly files. Handles apphosts (companion .dll redirect), -/// single-file bundles (entry assembly extraction), Native AOT binaries, and direct +/// single-file bundles (entry assembly extraction), Native AOT binaries, raw Wasm modules, and direct /// .dll/.exe loading. Returns an that preserves the /// distinction so callers can decide how to present each case (e.g. showing an apphost dialog). /// @@ -19,7 +19,8 @@ public static class AssemblyLoader /// for regular assemblies, /// for native apphosts with a companion .dll, /// for single-file bundles, - /// or for Native AOT compiled binaries. + /// for Native AOT compiled binaries, + /// or for raw Wasm modules. /// public static AssemblyOpenResult Open(string filePath) { diff --git a/src/Dotsider.Core/Analysis/Disasm/NativeDisassembler.cs b/src/Dotsider.Core/Analysis/Disasm/NativeDisassembler.cs index 0391f486..c58b0c5f 100644 --- a/src/Dotsider.Core/Analysis/Disasm/NativeDisassembler.cs +++ b/src/Dotsider.Core/Analysis/Disasm/NativeDisassembler.cs @@ -148,6 +148,9 @@ internal static (string Text, IReadOnlyList Instructions, int if (fileOffset < 0 || fileOffset + symbol.Size > raw.Length) return null; var code = raw.Span.Slice((int)fileOffset, (int)symbol.Size).ToArray(); + if (arch == NativeArchitecture.Wasm32 && codeImage.WasmModuleInfo is not null) + return Wasm.WasmDisassembler.DisassembleSymbol(codeImage, symbol); + // Compose the symbol resolver with the import resolver so indirect targets that land on an // import slot render as MODULE!Function rather than an unresolved address. The import table // is parsed once per image and cached — rebuilding it per call would re-read the whole PE @@ -203,6 +206,13 @@ bool Resolver(ulong va, out NativeSymbolRef sym) /// The address or name to resolve. public static IReadOnlyList FindExecutableSymbols(NativeSymbolInfo info, string target) { + if (info.Source == NativeSymbolSource.WebAssembly) + { + var wasmMatches = FindWasmSymbols(info, target); + if (wasmMatches.Count > 0) + return wasmMatches; + } + if (target.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? ulong.TryParse(target.AsSpan(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var va) : ulong.TryParse(target, out va)) @@ -222,9 +232,27 @@ public static IReadOnlyList FindExecutableSymbols(NativeSymbolInfo List raw = [.. executables.Where(s => string.Equals(s.Name, target, StringComparison.Ordinal))]; if (raw.Count > 0) return raw; + List alias = [.. executables.Where(s => s.Aliases.Contains(target, StringComparer.Ordinal))]; + if (alias.Count > 0) return alias; + return [.. executables.Where(s => (s.ManagedName ?? s.Name).EndsWith(target, StringComparison.Ordinal))]; } + private static IReadOnlyList FindWasmSymbols(NativeSymbolInfo info, string target) + { + var normalized = target.StartsWith("func:", StringComparison.OrdinalIgnoreCase) + ? target + : uint.TryParse(target, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index) + ? $"func:{index}" + : null; + if (normalized is null) + return []; + + return [.. info.Symbols.Where(s => + s.Kind == NativeSymbolKind.Function + && s.Aliases.Contains(normalized, StringComparer.Ordinal))]; + } + private static readonly ConditionalWeakTable> ImportResolverCache = []; private static NativeImportResolver? ImportResolverFor(AssemblyAnalyzer analyzer) => @@ -468,7 +496,7 @@ private static bool MayWriteFirstRegister(string mnemonic) => /// The synthesized label name for an intra-function target address. internal static string LocalLabel(ulong address) => $"loc_{address:x}"; - private static (string, IReadOnlyList, int) Render( + internal static (string, IReadOnlyList, int) Render( IReadOnlyList instructions, string? header) { var lines = new List(); diff --git a/src/Dotsider.Core/Analysis/Models/AssemblyOpenResult.cs b/src/Dotsider.Core/Analysis/Models/AssemblyOpenResult.cs index 4c4860fd..d90bc594 100644 --- a/src/Dotsider.Core/Analysis/Models/AssemblyOpenResult.cs +++ b/src/Dotsider.Core/Analysis/Models/AssemblyOpenResult.cs @@ -8,8 +8,8 @@ namespace Dotsider.Core.Analysis.Models; public abstract record AssemblyOpenResult { /// - /// Direct load — the file is a .dll or .exe with metadata, or a native binary - /// with no metadata and no ReadyToRun header (unknown format). + /// Direct load — the file is a .dll or .exe with metadata, a raw WebAssembly module, + /// or a native binary with no metadata and no ReadyToRun header (unknown format). /// /// The analyzer for the opened file. public sealed record Direct(AssemblyAnalyzer Analyzer) : AssemblyOpenResult; diff --git a/src/Dotsider.Core/Analysis/Models/BinaryKind.cs b/src/Dotsider.Core/Analysis/Models/BinaryKind.cs index c9e413e1..d983e3f8 100644 --- a/src/Dotsider.Core/Analysis/Models/BinaryKind.cs +++ b/src/Dotsider.Core/Analysis/Models/BinaryKind.cs @@ -15,6 +15,12 @@ public enum BinaryKind /// ReadyToRun, + /// + /// A WebAssembly module, most commonly the dotnet.native.wasm runtime module from a + /// .NET browser-wasm publish. It has native Wasm code but no ECMA-335 metadata of its own. + /// + Wasm, + /// /// A Native AOT compiled .NET binary: a native executable with no CLR metadata /// whose image embeds a validated ReadyToRun header. diff --git a/src/Dotsider.Core/Analysis/Models/NativeSymbolSource.cs b/src/Dotsider.Core/Analysis/Models/NativeSymbolSource.cs index cb81bd0d..fae936a4 100644 --- a/src/Dotsider.Core/Analysis/Models/NativeSymbolSource.cs +++ b/src/Dotsider.Core/Analysis/Models/NativeSymbolSource.cs @@ -32,5 +32,11 @@ public enum NativeSymbolSource /// A crossgen2 ReadyToRun image's method entry-point tables: named, sized function ranges /// (one per hot/funclet/cold runtime function) recovered directly from the R2R sections. /// - ReadyToRun + ReadyToRun, + + /// + /// A WebAssembly module's function/code sections, optionally named from + /// dotnet.native.js.symbols, the Wasm name section, or exports. + /// + WebAssembly } diff --git a/src/Dotsider.Core/Analysis/Models/WasmDataSegmentInfo.cs b/src/Dotsider.Core/Analysis/Models/WasmDataSegmentInfo.cs new file mode 100644 index 00000000..aede9d25 --- /dev/null +++ b/src/Dotsider.Core/Analysis/Models/WasmDataSegmentInfo.cs @@ -0,0 +1,10 @@ +namespace Dotsider.Core.Analysis.Models; + +/// +/// One WebAssembly data segment. +/// +/// The data segment index. +/// The decoded segment mode: active, passive, or active-explicit-memory. +/// The file offset where the segment's bytes begin. +/// The segment byte size. +public sealed record WasmDataSegmentInfo(int Index, string Mode, long FileOffset, int Size); diff --git a/src/Dotsider.Core/Analysis/Models/WasmElementSegmentInfo.cs b/src/Dotsider.Core/Analysis/Models/WasmElementSegmentInfo.cs new file mode 100644 index 00000000..ed05b922 --- /dev/null +++ b/src/Dotsider.Core/Analysis/Models/WasmElementSegmentInfo.cs @@ -0,0 +1,16 @@ +namespace Dotsider.Core.Analysis.Models; + +/// +/// One WebAssembly element segment declaration. +/// +/// The zero-based element segment index. +/// The decoded element segment mode. +/// The table index when the mode records one. +/// The reference type or element kind. +/// The number of recorded element expressions or indices. +public sealed record WasmElementSegmentInfo( + int Index, + string Mode, + int? TableIndex, + string ElementType, + int ElementCount); diff --git a/src/Dotsider.Core/Analysis/Models/WasmExportInfo.cs b/src/Dotsider.Core/Analysis/Models/WasmExportInfo.cs new file mode 100644 index 00000000..6541f25f --- /dev/null +++ b/src/Dotsider.Core/Analysis/Models/WasmExportInfo.cs @@ -0,0 +1,9 @@ +namespace Dotsider.Core.Analysis.Models; + +/// +/// One WebAssembly export entry. +/// +/// The exported name. +/// The exported external kind. +/// The exported index in the kind's index space. +public sealed record WasmExportInfo(string Name, WasmExternalKind Kind, int Index); diff --git a/src/Dotsider.Core/Analysis/Models/WasmExternalKind.cs b/src/Dotsider.Core/Analysis/Models/WasmExternalKind.cs new file mode 100644 index 00000000..2bba1498 --- /dev/null +++ b/src/Dotsider.Core/Analysis/Models/WasmExternalKind.cs @@ -0,0 +1,25 @@ +namespace Dotsider.Core.Analysis.Models; + +/// +/// The external item kind used by WebAssembly import and export sections. +/// +public enum WasmExternalKind +{ + /// A function index. + Function, + + /// A table index. + Table, + + /// A memory index. + Memory, + + /// A global index. + Global, + + /// An exception tag index. + Tag, + + /// An unrecognized external kind. + Unknown +} diff --git a/src/Dotsider.Core/Analysis/Models/WasmFunctionInfo.cs b/src/Dotsider.Core/Analysis/Models/WasmFunctionInfo.cs new file mode 100644 index 00000000..15809158 --- /dev/null +++ b/src/Dotsider.Core/Analysis/Models/WasmFunctionInfo.cs @@ -0,0 +1,38 @@ +namespace Dotsider.Core.Analysis.Models; + +/// +/// A WebAssembly function, imported or defined in the code section. +/// +/// The module-wide function index, including imported functions. +/// The function type index, when known. +/// The best display name for the function. +/// Where the display name came from. +/// Whether the function is imported and has no body in this module. +/// The import module for imported functions. +/// The import name for imported functions. +/// Whether the function is exported. +/// All export names that point at this function. +/// The file offset of the function body payload, including local declarations. +/// The body payload size in bytes. +/// The file offset of the first instruction byte after local declarations. +/// The instruction byte count after local declarations. +/// The function's run-length encoded local declarations. +/// The raw Wasm parameter type bytes. +/// The raw Wasm result type bytes. +public sealed record WasmFunctionInfo( + int Index, + int? TypeIndex, + string Name, + string NameSource, + bool IsImported, + string? ImportModule, + string? ImportName, + bool IsExported, + IReadOnlyList ExportNames, + long? BodyOffset, + int BodySize, + long? CodeOffset, + int CodeSize, + IReadOnlyList Locals, + IReadOnlyList ParamTypes, + IReadOnlyList ResultTypes); diff --git a/src/Dotsider.Core/Analysis/Models/WasmGlobalInfo.cs b/src/Dotsider.Core/Analysis/Models/WasmGlobalInfo.cs new file mode 100644 index 00000000..79e1a43f --- /dev/null +++ b/src/Dotsider.Core/Analysis/Models/WasmGlobalInfo.cs @@ -0,0 +1,14 @@ +namespace Dotsider.Core.Analysis.Models; + +/// +/// One WebAssembly global declaration. +/// +/// The zero-based global index. +/// The raw WebAssembly value-type byte. +/// The display name for the value type. +/// Whether the global is mutable. +public sealed record WasmGlobalInfo( + int Index, + byte ValueType, + string ValueTypeName, + bool IsMutable); diff --git a/src/Dotsider.Core/Analysis/Models/WasmImportInfo.cs b/src/Dotsider.Core/Analysis/Models/WasmImportInfo.cs new file mode 100644 index 00000000..794887c4 --- /dev/null +++ b/src/Dotsider.Core/Analysis/Models/WasmImportInfo.cs @@ -0,0 +1,16 @@ +namespace Dotsider.Core.Analysis.Models; + +/// +/// One WebAssembly import entry. +/// +/// The imported module name. +/// The imported item name. +/// The imported external kind. +/// The import's index within its index space when applicable. +/// The function type index for function imports, or null. +public sealed record WasmImportInfo( + string ModuleName, + string Name, + WasmExternalKind Kind, + int Index, + int? TypeIndex); diff --git a/src/Dotsider.Core/Analysis/Models/WasmLocalInfo.cs b/src/Dotsider.Core/Analysis/Models/WasmLocalInfo.cs new file mode 100644 index 00000000..6c004df5 --- /dev/null +++ b/src/Dotsider.Core/Analysis/Models/WasmLocalInfo.cs @@ -0,0 +1,9 @@ +namespace Dotsider.Core.Analysis.Models; + +/// +/// A run-length encoded local declaration inside a WebAssembly function body. +/// +/// The number of locals in this run. +/// The raw Wasm value type byte. +/// The display name of the value type. +public sealed record WasmLocalInfo(uint Count, byte ValueType, string DisplayType); diff --git a/src/Dotsider.Core/Analysis/Models/WasmMemoryInfo.cs b/src/Dotsider.Core/Analysis/Models/WasmMemoryInfo.cs new file mode 100644 index 00000000..d25db5d9 --- /dev/null +++ b/src/Dotsider.Core/Analysis/Models/WasmMemoryInfo.cs @@ -0,0 +1,16 @@ +namespace Dotsider.Core.Analysis.Models; + +/// +/// One WebAssembly memory declaration. +/// +/// The zero-based memory index. +/// The minimum memory page count. +/// The maximum memory page count when one is declared. +/// Whether the memory is declared shared. +/// Whether the memory uses 64-bit indices. +public sealed record WasmMemoryInfo( + int Index, + ulong MinimumPages, + ulong? MaximumPages, + bool IsShared, + bool IsMemory64); diff --git a/src/Dotsider.Core/Analysis/Models/WasmModuleInfo.cs b/src/Dotsider.Core/Analysis/Models/WasmModuleInfo.cs new file mode 100644 index 00000000..0bea459c --- /dev/null +++ b/src/Dotsider.Core/Analysis/Models/WasmModuleInfo.cs @@ -0,0 +1,59 @@ +namespace Dotsider.Core.Analysis.Models; + +/// +/// Parsed facts for a WebAssembly module, including its functions and optional .NET symbol map. +/// +/// The WebAssembly binary version. +/// The section table in file order. +/// The parsed function type entries. +/// The parsed import entries. +/// The parsed export entries. +/// Imported and defined functions in function-index order. +/// The parsed table declarations. +/// The parsed memory declarations. +/// The parsed global declarations. +/// The parsed element segments. +/// The parsed data segments. +/// The parsed exception tag declarations. +/// The start function index, when present. +/// The data-count section value, when present. +/// Feature names from the target_features custom section. +/// Producer strings from the producers custom section. +/// The symbol-map sidecar path, when loaded. +/// The symbol-map probe outcome. +/// The number of parsed sidecar entries. +/// A diagnostic note when parsing had to degrade, or null. +public sealed record WasmModuleInfo( + int Version, + IReadOnlyList Sections, + IReadOnlyList Types, + IReadOnlyList Imports, + IReadOnlyList Exports, + IReadOnlyList Functions, + IReadOnlyList Tables, + IReadOnlyList Memories, + IReadOnlyList Globals, + IReadOnlyList Elements, + IReadOnlyList DataSegments, + IReadOnlyList Tags, + int? StartFunctionIndex, + int? DataCount, + IReadOnlyList TargetFeatures, + IReadOnlyList ProducerFields, + string? SymbolMapPath, + WasmSymbolMapStatus SymbolMapStatus, + int SymbolMapEntryCount, + string? Diagnostic) +{ + /// The number of imported functions in the module. + public int ImportedFunctionCount => Functions.Count(f => f.IsImported); + + /// The number of defined functions with code bodies in the module. + public int DefinedFunctionCount => Functions.Count(f => !f.IsImported); + + /// The total byte count of all defined function instruction streams. + public long CodeSize => Functions.Sum(f => (long)f.CodeSize); + + /// The total byte count of all data segments. + public long DataSize => DataSegments.Sum(s => (long)s.Size); +} diff --git a/src/Dotsider.Core/Analysis/Models/WasmSectionInfo.cs b/src/Dotsider.Core/Analysis/Models/WasmSectionInfo.cs new file mode 100644 index 00000000..41448eb8 --- /dev/null +++ b/src/Dotsider.Core/Analysis/Models/WasmSectionInfo.cs @@ -0,0 +1,14 @@ +namespace Dotsider.Core.Analysis.Models; + +/// +/// One section in a WebAssembly module, including custom sections. +/// +/// The numeric section id. +/// The standard section name, or the custom section name for id 0. +/// The file offset where section payload bytes begin. +/// The section payload size in bytes. +public sealed record WasmSectionInfo( + byte Id, + string Name, + long FileOffset, + int Size); diff --git a/src/Dotsider.Core/Analysis/Models/WasmSymbolMapStatus.cs b/src/Dotsider.Core/Analysis/Models/WasmSymbolMapStatus.cs new file mode 100644 index 00000000..c0fe13c4 --- /dev/null +++ b/src/Dotsider.Core/Analysis/Models/WasmSymbolMapStatus.cs @@ -0,0 +1,16 @@ +namespace Dotsider.Core.Analysis.Models; + +/// +/// The outcome of probing a WebAssembly module's dotnet.native.js.symbols sidecar. +/// +public enum WasmSymbolMapStatus +{ + /// No sidecar was expected or found. + Missing, + + /// The sidecar was found and parsed. + Loaded, + + /// The sidecar was found but no valid entries could be read. + Corrupt +} diff --git a/src/Dotsider.Core/Analysis/Models/WasmTableInfo.cs b/src/Dotsider.Core/Analysis/Models/WasmTableInfo.cs new file mode 100644 index 00000000..b02aa582 --- /dev/null +++ b/src/Dotsider.Core/Analysis/Models/WasmTableInfo.cs @@ -0,0 +1,14 @@ +namespace Dotsider.Core.Analysis.Models; + +/// +/// One WebAssembly table declaration. +/// +/// The zero-based table index. +/// The table reference type. +/// The minimum element count. +/// The maximum element count when one is declared. +public sealed record WasmTableInfo( + int Index, + string RefType, + ulong Minimum, + ulong? Maximum); diff --git a/src/Dotsider.Core/Analysis/Models/WasmTagInfo.cs b/src/Dotsider.Core/Analysis/Models/WasmTagInfo.cs new file mode 100644 index 00000000..40ffd698 --- /dev/null +++ b/src/Dotsider.Core/Analysis/Models/WasmTagInfo.cs @@ -0,0 +1,12 @@ +namespace Dotsider.Core.Analysis.Models; + +/// +/// One WebAssembly exception tag declaration. +/// +/// The zero-based tag index. +/// The tag attribute value. +/// The function type index used by the tag. +public sealed record WasmTagInfo( + int Index, + uint Attribute, + int TypeIndex); diff --git a/src/Dotsider.Core/Analysis/Models/WasmTypeInfo.cs b/src/Dotsider.Core/Analysis/Models/WasmTypeInfo.cs new file mode 100644 index 00000000..ca146c33 --- /dev/null +++ b/src/Dotsider.Core/Analysis/Models/WasmTypeInfo.cs @@ -0,0 +1,12 @@ +namespace Dotsider.Core.Analysis.Models; + +/// +/// One WebAssembly function type from the type section. +/// +/// The zero-based type index. +/// The raw WebAssembly parameter value-type bytes. +/// The raw WebAssembly result value-type bytes. +public sealed record WasmTypeInfo( + int Index, + IReadOnlyList ParamTypes, + IReadOnlyList ResultTypes); diff --git a/src/Dotsider.Core/Analysis/Models/WebcilInfo.cs b/src/Dotsider.Core/Analysis/Models/WebcilInfo.cs new file mode 100644 index 00000000..18b9d9af --- /dev/null +++ b/src/Dotsider.Core/Analysis/Models/WebcilInfo.cs @@ -0,0 +1,22 @@ +namespace Dotsider.Core.Analysis.Models; + +/// +/// Parsed provenance for a Webcil managed assembly, including whether it was wrapped inside +/// a WebAssembly module. Webcil is a .NET metadata container used by browser-wasm publishes, +/// so dotsider routes it through the managed metadata and IL experience. +/// +/// The Webcil major format version. +/// The Webcil minor format version. +/// True when the Webcil payload was found inside a Wasm wrapper module. +/// The file offset of the Webcil payload in the opened file. +/// The number of Webcil section records. +/// The size of the ECMA-335 metadata blob. +/// The size of the Webcil debug directory, when present. +public sealed record WebcilInfo( + int VersionMajor, + int VersionMinor, + bool IsWasmWrapped, + long PayloadOffset, + int SectionCount, + int MetadataSize, + int DebugDirectorySize); diff --git a/src/Dotsider.Core/Analysis/SizeAnalyzer.cs b/src/Dotsider.Core/Analysis/SizeAnalyzer.cs index 7f7384ad..7d14d7f5 100644 --- a/src/Dotsider.Core/Analysis/SizeAnalyzer.cs +++ b/src/Dotsider.Core/Analysis/SizeAnalyzer.cs @@ -32,6 +32,9 @@ public static SizeNode BuildSizeTree(AssemblyAnalyzer analyzer) if (analyzer.IsReadyToRun && analyzer.ReadyToRunIndex is { Methods.Count: > 0 }) return BuildReadyToRunSizeTree(analyzer); + if (analyzer.BinaryKind == BinaryKind.Wasm && analyzer.WasmModuleInfo is { } wasm) + return BuildWasmSizeTree(analyzer, wasm); + // Get method sizes var methodSizes = new List<(MethodDefInfo Method, long Size)>(); foreach (var method in analyzer.MethodDefs) @@ -113,6 +116,111 @@ public static SizeNode BuildSizeTree(AssemblyAnalyzer analyzer) [.. namespaceNodes.OrderByDescending(n => n.Size)]); } + /// + /// Builds the size tree of a raw WebAssembly module from its function bodies, data segments, + /// and remaining section payload bytes. Function and data entries are file-backed slices, so + /// the map answers where the SDK-produced dotnet.native.wasm bytes went. + /// + private static SizeNode BuildWasmSizeTree(AssemblyAnalyzer analyzer, WasmModuleInfo wasm) + { + var roots = new List(); + + var functionNodes = wasm.Functions + .Where(static f => !f.IsImported && f.BodySize > 0 && f.BodyOffset is not null) + .OrderByDescending(static f => f.BodySize) + .Select(static f => new SizeNode( + f.Name, + $"Functions/{f.Name}@0x{f.BodyOffset!.Value:x}", + f.BodySize, + SizeNodeKind.Function, + [])) + .ToList(); + + var codePayloadSize = wasm.Sections.FirstOrDefault(static s => s.Id == 10)?.Size ?? 0; + var functionPayloadSize = functionNodes.Sum(static n => n.Size); + if (codePayloadSize > functionPayloadSize) + { + functionNodes.Add(new SizeNode( + "Code section overhead", + "Functions/Code section overhead", + codePayloadSize - functionPayloadSize, + SizeNodeKind.Blob, + [])); + } + + if (functionNodes.Count > 0) + { + roots.Add(new SizeNode( + "Functions", + "Functions", + functionNodes.Sum(static n => n.Size), + SizeNodeKind.Category, + [.. functionNodes.OrderByDescending(static n => n.Size)])); + } + + var dataNodes = wasm.DataSegments + .Where(static d => d.Size > 0) + .OrderByDescending(static d => d.Size) + .Select(static d => new SizeNode( + $"segment {d.Index} ({d.Mode})", + $"Data/segment-{d.Index}@0x{d.FileOffset:x}", + d.Size, + SizeNodeKind.Blob, + [])) + .ToList(); + + var dataPayloadSize = wasm.Sections.FirstOrDefault(static s => s.Id == 11)?.Size ?? 0; + var dataSegmentSize = dataNodes.Sum(static n => n.Size); + if (dataPayloadSize > dataSegmentSize) + { + dataNodes.Add(new SizeNode( + "Data section overhead", + "Data/Data section overhead", + dataPayloadSize - dataSegmentSize, + SizeNodeKind.Blob, + [])); + } + + if (dataNodes.Count > 0) + { + roots.Add(new SizeNode( + "Data", + "Data", + dataNodes.Sum(static n => n.Size), + SizeNodeKind.Category, + [.. dataNodes.OrderByDescending(static n => n.Size)])); + } + + var accountedSectionIds = new HashSet { 10, 11 }; + var sectionNodes = wasm.Sections + .Where(s => s.Size > 0 && !accountedSectionIds.Contains(s.Id)) + .OrderByDescending(static s => s.Size) + .Select(static s => new SizeNode( + s.Name, + $"Sections/{s.Name}@0x{s.FileOffset:x}", + s.Size, + SizeNodeKind.Blob, + [])) + .ToList(); + + if (sectionNodes.Count > 0) + { + roots.Add(new SizeNode( + "Sections", + "Sections", + sectionNodes.Sum(static n => n.Size), + SizeNodeKind.Category, + sectionNodes)); + } + + return new SizeNode( + $"{analyzer.FileName} (Wasm)", + analyzer.FileName, + roots.Sum(static r => r.Size), + SizeNodeKind.Assembly, + [.. roots.OrderByDescending(static r => r.Size)]); + } + /// /// Builds the size tree of a ReadyToRun image from its precompiled method sizes: namespace → /// type → method, each method sized by its total native code across all ranges. Distinct from diff --git a/src/Dotsider.Core/Analysis/Wasm/WasmDisassembler.cs b/src/Dotsider.Core/Analysis/Wasm/WasmDisassembler.cs new file mode 100644 index 00000000..9a714f76 --- /dev/null +++ b/src/Dotsider.Core/Analysis/Wasm/WasmDisassembler.cs @@ -0,0 +1,230 @@ +using Dotsider.Core.Analysis.Disasm; +using Dotsider.Core.Analysis.Models; + +namespace Dotsider.Core.Analysis.Wasm; + +/// +/// Disassembles WebAssembly function bodies with function-index-aware target naming. +/// +internal static class WasmDisassembler +{ + /// + /// Disassembles one Wasm function symbol from its module. + /// + /// The analyzer that opened the raw WebAssembly module. + /// The file-backed Wasm function symbol to disassemble. + /// A rendered listing, decoded instructions, and header line count, or null. + public static (string Text, IReadOnlyList Instructions, int HeaderLineCount)? + DisassembleSymbol(AssemblyAnalyzer analyzer, NativeSymbol symbol) + { + if (analyzer.WasmModuleInfo is not { } module || symbol.FileOffset is not { } fileOffset || symbol.Size <= 0) + return null; + + var raw = analyzer.RawBytes.Span; + if (fileOffset < 0 || fileOffset + symbol.Size > raw.Length) + return null; + + var functionByIndex = module.Functions.ToDictionary(static f => f.Index); + var function = module.Functions.FirstOrDefault(f => f.CodeOffset == fileOffset); + var code = raw.Slice((int)fileOffset, (int)symbol.Size); + var decoded = NativeDisassembler.Disassemble(code, symbol.VirtualAddress, NativeArchitecture.Wasm32); + var instructions = new List(decoded.Count); + + foreach (var insn in decoded) + { + var withOffset = insn with + { + FileOffset = fileOffset + (long)(insn.Address - symbol.VirtualAddress), + }; + + instructions.Add(ResolveWasmTarget(withOffset, module, function, functionByIndex)); + } + + var header = function is null + ? symbol.Name + : $"func[{function.Index}] {function.Name}\n// type: {FormatSignature(function)}"; + return NativeDisassembler.Render(instructions, header); + } + + private static NativeInstruction ResolveWasmTarget( + NativeInstruction instruction, + WasmModuleInfo module, + WasmFunctionInfo? function, + Dictionary functions) + { + return instruction.Mnemonic switch + { + "call" or "return_call" => ResolveDirectCall(instruction, functions), + "call_indirect" or "return_call_indirect" => AnnotateIndirectCall(instruction, module), + "call_ref" or "return_call_ref" => AnnotateTypeOperand(instruction, module, 0), + "local.get" or "local.set" or "local.tee" => AnnotateLocalOperand(instruction, function), + "global.get" or "global.set" => AnnotateGlobalOperand(instruction, module), + "table.get" or "table.set" => AnnotateTableOperand(instruction, module), + _ => instruction, + }; + } + + private static NativeInstruction ResolveDirectCall( + NativeInstruction instruction, + Dictionary functions) + { + if (!TryGetOperandIndex(instruction, 0, out var rawIndex) + || !functions.TryGetValue(rawIndex, out var target)) + return instruction; + + var operand = instruction.Operands[0]; + var namedOperand = operand with { Text = $"{operand.Text} <{target.Name}>" }; + var operands = instruction.Operands.ToArray(); + operands[0] = namedOperand; + + if (target.IsImported) + { + return instruction with + { + Operands = operands, + OperandText = string.Join(", ", operands.Select(static op => op.Text)), + TargetKind = NativeTargetKind.Import, + TargetName = target.Name, + }; + } + + return instruction with + { + Operands = operands, + OperandText = string.Join(", ", operands.Select(static op => op.Text)), + TargetAddress = target.CodeOffset is { } offset ? (ulong)offset : null, + TargetKind = NativeTargetKind.Function, + TargetName = target.Name, + }; + } + + private static NativeInstruction AnnotateIndirectCall(NativeInstruction instruction, WasmModuleInfo module) + { + var current = AnnotateTypeOperand(instruction, module, 0); + return AnnotateTableOperand(current, module, operandIndex: 1); + } + + private static NativeInstruction AnnotateTypeOperand( + NativeInstruction instruction, WasmModuleInfo module, int operandIndex) + { + if (!TryGetOperandIndex(instruction, operandIndex, out var typeIndex)) + return instruction; + var type = module.Types.FirstOrDefault(t => t.Index == typeIndex); + if (type is null) + return instruction; + + return ReplaceOperandText(instruction, operandIndex, + text => $"{text} <{FormatSignature(type.ParamTypes, type.ResultTypes)}>"); + } + + private static NativeInstruction AnnotateLocalOperand( + NativeInstruction instruction, WasmFunctionInfo? function) + { + if (function is null || !TryGetOperandIndex(instruction, 0, out var index)) + return instruction; + + var paramCount = function.ParamTypes.Count; + if (index < paramCount) + return ReplaceOperandText(instruction, 0, + text => $"{text} "); + + var localIndex = index - paramCount; + foreach (var localRun in function.Locals) + { + if (localIndex >= 0 && (uint)localIndex < localRun.Count) + return ReplaceOperandText(instruction, 0, + text => $"{text} "); + localIndex -= checked((int)localRun.Count); + } + + return instruction; + } + + private static NativeInstruction AnnotateGlobalOperand(NativeInstruction instruction, WasmModuleInfo module) + { + if (!TryGetOperandIndex(instruction, 0, out var index)) + return instruction; + + var imported = module.Imports.FirstOrDefault(i => i.Kind == WasmExternalKind.Global && i.Index == index); + if (imported is not null) + return ReplaceOperandText(instruction, 0, text => $"{text} <{imported.ModuleName}!{imported.Name}>"); + + var defined = module.Globals.FirstOrDefault(g => g.Index == index); + if (defined is not null) + return ReplaceOperandText(instruction, 0, + text => $"{text} "); + + return instruction; + } + + private static NativeInstruction AnnotateTableOperand( + NativeInstruction instruction, WasmModuleInfo module, int operandIndex = 0) + { + if (!TryGetOperandIndex(instruction, operandIndex, out var index)) + return instruction; + + var imported = module.Imports.FirstOrDefault(i => i.Kind == WasmExternalKind.Table && i.Index == index); + if (imported is not null) + return ReplaceOperandText(instruction, operandIndex, text => $"{text} <{imported.ModuleName}!{imported.Name}>"); + + var table = module.Tables.FirstOrDefault(t => t.Index == index); + if (table is not null) + return ReplaceOperandText(instruction, operandIndex, + text => $"{text} "); + + return instruction; + } + + private static NativeInstruction ReplaceOperandText( + NativeInstruction instruction, int operandIndex, Func replace) + { + if (operandIndex < 0 || operandIndex >= instruction.Operands.Count) + return instruction; + + var operands = instruction.Operands.ToArray(); + operands[operandIndex] = operands[operandIndex] with { Text = replace(operands[operandIndex].Text) }; + return instruction with + { + Operands = operands, + OperandText = string.Join(", ", operands.Select(static op => op.Text)), + }; + } + + private static bool TryGetOperandIndex(NativeInstruction instruction, int operandIndex, out int index) + { + index = 0; + if (operandIndex < 0 + || operandIndex >= instruction.Operands.Count + || instruction.Operands[operandIndex].Immediate is not { } rawIndex + || rawIndex < 0 + || rawIndex > int.MaxValue) + { + return false; + } + + index = (int)rawIndex; + return true; + } + + private static string FormatSignature(WasmFunctionInfo function) + => FormatSignature(function.ParamTypes, function.ResultTypes); + + private static string FormatSignature(IReadOnlyList paramTypes, IReadOnlyList resultTypes) + { + var parameters = string.Join(", ", paramTypes.Select(ValueTypeName)); + var results = string.Join(", ", resultTypes.Select(ValueTypeName)); + return $"({parameters}) -> ({results})"; + } + + private static string ValueTypeName(byte valueType) => valueType switch + { + 0x7F => "i32", + 0x7E => "i64", + 0x7D => "f32", + 0x7C => "f64", + 0x7B => "v128", + 0x70 => "funcref", + 0x6F => "externref", + _ => $"0x{valueType:X2}", + }; +} diff --git a/src/Dotsider.Core/Analysis/Wasm/WasmModuleReader.cs b/src/Dotsider.Core/Analysis/Wasm/WasmModuleReader.cs new file mode 100644 index 00000000..8a27ada3 --- /dev/null +++ b/src/Dotsider.Core/Analysis/Wasm/WasmModuleReader.cs @@ -0,0 +1,1019 @@ +using Dotsider.Core.Analysis.Models; +using System.Buffers.Binary; +using System.Globalization; +using System.Text; + +namespace Dotsider.Core.Analysis.Wasm; + +/// +/// Parses WebAssembly modules produced by .NET browser-wasm publishes. +/// +internal static class WasmModuleReader +{ + private const uint WasmMagic = 0x6D736100; + private const uint WasmVersion = 1; + private const uint WebcilMagic = 0x4C496257; + + /// + /// Returns true when the bytes start with the WebAssembly binary magic and version. + /// + /// The candidate file bytes. + /// True when the bytes are a WebAssembly 1.0 binary module. + public static bool IsWasmModule(ReadOnlySpan bytes) => + bytes.Length >= 8 + && BinaryPrimitives.ReadUInt32LittleEndian(bytes) == WasmMagic + && BinaryPrimitives.ReadUInt32LittleEndian(bytes[4..]) == WasmVersion; + + /// + /// Reads a WebAssembly module, preserving partial results when optional sections are malformed. + /// + /// The WebAssembly module bytes. + /// The source path used to locate SDK symbol-map sidecars. + /// Parsed module facts, functions, data segments, and sidecar status. + public static WasmModuleInfo Read(ReadOnlySpan bytes, string? filePath) + { + if (!IsWasmModule(bytes)) + throw new BadImageFormatException("The file is not a WebAssembly 1.0 module."); + + var sections = new List(); + var types = new List(); + var imports = new List(); + var functionImports = new List(); + var functionTypeIndices = new List(); + var bodies = new List(); + var exports = new List(); + var tables = new List(); + var memories = new List(); + var globals = new List(); + var elements = new List(); + var functionNames = new Dictionary(); + var dataSegments = new List(); + var tags = new List(); + var targetFeatures = new List(); + var producers = new List(); + int? startFunctionIndex = null; + int? dataCount = null; + string? diagnostic = null; + + var pos = 8; + while (pos < bytes.Length) + { + try + { + var sectionId = ReadByte(bytes, ref pos); + var sectionSize = checked((int)ReadUleb(bytes, ref pos)); + var sectionPayloadOffset = pos; + var sectionEnd = checked(pos + sectionSize); + if (sectionEnd > bytes.Length) + throw new InvalidDataException("A WebAssembly section extends past the end of the file."); + + var sectionName = StandardSectionName(sectionId); + if (sectionId == 0) + { + var customPos = sectionPayloadOffset; + sectionName = ReadName(bytes, ref customPos, sectionEnd); + ParseCustomSection(sectionName, bytes, customPos, sectionEnd, functionNames, targetFeatures, producers); + } + else + { + var sectionPos = sectionPayloadOffset; + switch (sectionId) + { + case 1: + types = ReadTypeSection(bytes, ref sectionPos, sectionEnd); + break; + case 2: + ReadImportSection(bytes, ref sectionPos, sectionEnd, imports, functionImports); + break; + case 3: + functionTypeIndices = ReadFunctionSection(bytes, ref sectionPos, sectionEnd); + break; + case 4: + tables = ReadTableSection( + bytes, ref sectionPos, sectionEnd, + imports.Count(static i => i.Kind == WasmExternalKind.Table)); + break; + case 5: + memories = ReadMemorySection( + bytes, ref sectionPos, sectionEnd, + imports.Count(static i => i.Kind == WasmExternalKind.Memory)); + break; + case 6: + globals = ReadGlobalSection( + bytes, ref sectionPos, sectionEnd, + imports.Count(static i => i.Kind == WasmExternalKind.Global)); + break; + case 7: + exports = ReadExportSection(bytes, ref sectionPos, sectionEnd); + break; + case 8: + startFunctionIndex = ReadStartSection(bytes, ref sectionPos, sectionEnd); + break; + case 9: + elements = ReadElementSection(bytes, ref sectionPos, sectionEnd); + break; + case 10: + bodies = ReadCodeSection(bytes, ref sectionPos, sectionEnd, sectionPayloadOffset); + break; + case 11: + dataSegments = ReadDataSection(bytes, ref sectionPos, sectionEnd); + break; + case 12: + dataCount = ReadDataCountSection(bytes, ref sectionPos, sectionEnd); + break; + case 13: + tags = ReadTagSection( + bytes, ref sectionPos, sectionEnd, + imports.Count(static i => i.Kind == WasmExternalKind.Tag)); + break; + } + } + + sections.Add(new WasmSectionInfo(sectionId, sectionName, sectionPayloadOffset, sectionSize)); + pos = sectionEnd; + } + catch (Exception ex) when (ex is InvalidDataException or OverflowException or ArgumentOutOfRangeException) + { + diagnostic = ex.Message; + break; + } + } + + var (symbolMapPath, symbolMapStatus, symbolMapEntries) = ReadSymbolMap(filePath); + var functions = BuildFunctions( + types, functionImports, functionTypeIndices, bodies, exports, functionNames, symbolMapEntries); + + return new WasmModuleInfo( + Version: (int)WasmVersion, + Sections: sections, + Types: types, + Imports: imports, + Exports: exports, + Functions: functions, + Tables: tables, + Memories: memories, + Globals: globals, + Elements: elements, + DataSegments: dataSegments, + Tags: tags, + StartFunctionIndex: startFunctionIndex, + DataCount: dataCount, + TargetFeatures: targetFeatures, + ProducerFields: producers, + SymbolMapPath: symbolMapPath, + SymbolMapStatus: symbolMapStatus, + SymbolMapEntryCount: symbolMapEntries.Count, + Diagnostic: diagnostic); + } + + /// + /// Finds a Webcil payload embedded as a passive Wasm data segment. + /// + public static bool TryExtractWebcilPayload(ReadOnlySpan bytes, out byte[] payload) + { + payload = []; + if (!IsWasmModule(bytes)) + return false; + + var pos = 8; + while (pos < bytes.Length) + { + var sectionId = ReadByte(bytes, ref pos); + var sectionSize = checked((int)ReadUleb(bytes, ref pos)); + var sectionEnd = checked(pos + sectionSize); + if (sectionEnd > bytes.Length) + return false; + + if (sectionId != 11) + { + pos = sectionEnd; + continue; + } + + var count = checked((int)ReadUleb(bytes, ref pos)); + for (var i = 0; i < count && pos < sectionEnd; i++) + { + var mode = checked((int)ReadUleb(bytes, ref pos)); + if (mode == 0) + SkipConstExpr(bytes, ref pos, sectionEnd); + else if (mode == 2) + { + _ = ReadUleb(bytes, ref pos); + SkipConstExpr(bytes, ref pos, sectionEnd); + } + + var size = checked((int)ReadUleb(bytes, ref pos)); + if (pos + size > sectionEnd) + return false; + + var segment = bytes[pos..(pos + size)]; + if (IsWebcilPayload(segment)) + { + payload = segment.ToArray(); + return true; + } + + pos += size; + } + + return false; + } + + return false; + } + + private static List ReadTypeSection(ReadOnlySpan bytes, ref int pos, int end) + { + var count = checked((int)ReadUleb(bytes, ref pos)); + var types = new List(count); + for (var i = 0; i < count; i++) + { + var form = ReadByte(bytes, ref pos); + if (form != 0x60) + throw new InvalidDataException("Only WebAssembly function types are supported."); + + var paramCount = checked((int)ReadUleb(bytes, ref pos)); + var parameters = new byte[paramCount]; + for (var p = 0; p < paramCount; p++) + parameters[p] = ReadByte(bytes, ref pos); + + var resultCount = checked((int)ReadUleb(bytes, ref pos)); + var results = new byte[resultCount]; + for (var r = 0; r < resultCount; r++) + results[r] = ReadByte(bytes, ref pos); + + types.Add(new WasmTypeInfo(i, parameters, results)); + } + + RequireSectionConsumed(pos, end); + return types; + } + + private static void ReadImportSection( + ReadOnlySpan bytes, ref int pos, int end, + List imports, List functionImports) + { + var count = checked((int)ReadUleb(bytes, ref pos)); + var tableIndex = 0; + var memoryIndex = 0; + var globalIndex = 0; + var tagIndex = 0; + for (var i = 0; i < count; i++) + { + var module = ReadName(bytes, ref pos, end); + var name = ReadName(bytes, ref pos, end); + var kindByte = ReadByte(bytes, ref pos); + var kind = ExternalKind(kindByte); + var index = kind switch + { + WasmExternalKind.Function => functionImports.Count, + WasmExternalKind.Table => tableIndex++, + WasmExternalKind.Memory => memoryIndex++, + WasmExternalKind.Global => globalIndex++, + WasmExternalKind.Tag => tagIndex++, + _ => i, + }; + + int? typeIndex = null; + switch (kind) + { + case WasmExternalKind.Function: + typeIndex = checked((int)ReadUleb(bytes, ref pos)); + break; + case WasmExternalKind.Table: + SkipTableType(bytes, ref pos); + break; + case WasmExternalKind.Memory: + SkipLimits(bytes, ref pos); + break; + case WasmExternalKind.Global: + _ = ReadByte(bytes, ref pos); + _ = ReadByte(bytes, ref pos); + break; + case WasmExternalKind.Tag: + _ = ReadUleb(bytes, ref pos); + typeIndex = checked((int)ReadUleb(bytes, ref pos)); + break; + default: + throw new InvalidDataException($"Unsupported WebAssembly import kind 0x{kindByte:X2}."); + } + + var import = new WasmImportInfo(module, name, kind, index, typeIndex); + imports.Add(import); + if (kind == WasmExternalKind.Function) + functionImports.Add(import); + } + + RequireSectionConsumed(pos, end); + } + + private static List ReadFunctionSection(ReadOnlySpan bytes, ref int pos, int end) + { + var count = checked((int)ReadUleb(bytes, ref pos)); + var result = new List(count); + for (var i = 0; i < count; i++) + result.Add(checked((int)ReadUleb(bytes, ref pos))); + + RequireSectionConsumed(pos, end); + return result; + } + + private static List ReadTableSection( + ReadOnlySpan bytes, ref int pos, int end, int importCount) + { + var count = checked((int)ReadUleb(bytes, ref pos)); + var result = new List(count); + for (var i = 0; i < count; i++) + { + var refType = ReadRefTypeName(bytes, ref pos); + var (minimum, maximum, _, _) = ReadLimits(bytes, ref pos); + result.Add(new WasmTableInfo(importCount + i, refType, minimum, maximum)); + } + + RequireSectionConsumed(pos, end); + return result; + } + + private static List ReadMemorySection( + ReadOnlySpan bytes, ref int pos, int end, int importCount) + { + var count = checked((int)ReadUleb(bytes, ref pos)); + var result = new List(count); + for (var i = 0; i < count; i++) + { + var (minimum, maximum, isShared, isMemory64) = ReadLimits(bytes, ref pos); + result.Add(new WasmMemoryInfo(importCount + i, minimum, maximum, isShared, isMemory64)); + } + + RequireSectionConsumed(pos, end); + return result; + } + + private static List ReadGlobalSection( + ReadOnlySpan bytes, ref int pos, int end, int importCount) + { + var count = checked((int)ReadUleb(bytes, ref pos)); + var result = new List(count); + for (var i = 0; i < count; i++) + { + var valueType = ReadByte(bytes, ref pos); + var mutable = ReadByte(bytes, ref pos); + SkipConstExpr(bytes, ref pos, end); + result.Add(new WasmGlobalInfo(importCount + i, valueType, ValueTypeName(valueType), mutable != 0)); + } + + RequireSectionConsumed(pos, end); + return result; + } + + private static List ReadExportSection(ReadOnlySpan bytes, ref int pos, int end) + { + var count = checked((int)ReadUleb(bytes, ref pos)); + var result = new List(count); + for (var i = 0; i < count; i++) + { + var name = ReadName(bytes, ref pos, end); + var kind = ExternalKind(ReadByte(bytes, ref pos)); + var index = checked((int)ReadUleb(bytes, ref pos)); + result.Add(new WasmExportInfo(name, kind, index)); + } + + RequireSectionConsumed(pos, end); + return result; + } + + private static int ReadStartSection(ReadOnlySpan bytes, ref int pos, int end) + { + var index = checked((int)ReadUleb(bytes, ref pos)); + RequireSectionConsumed(pos, end); + return index; + } + + private static List ReadElementSection(ReadOnlySpan bytes, ref int pos, int end) + { + var count = checked((int)ReadUleb(bytes, ref pos)); + var result = new List(count); + for (var i = 0; i < count; i++) + { + var flags = checked((int)ReadUleb(bytes, ref pos)); + string mode; + int? tableIndex = null; + string elementType; + int elementCount; + + switch (flags) + { + case 0: + mode = "active"; + tableIndex = 0; + SkipConstExpr(bytes, ref pos, end); + elementType = "funcref"; + elementCount = SkipFunctionIndexVector(bytes, ref pos); + break; + case 1: + mode = "passive"; + elementType = ElementKindName(ReadByte(bytes, ref pos)); + elementCount = SkipFunctionIndexVector(bytes, ref pos); + break; + case 2: + mode = "active-explicit-table"; + tableIndex = checked((int)ReadUleb(bytes, ref pos)); + SkipConstExpr(bytes, ref pos, end); + elementType = ElementKindName(ReadByte(bytes, ref pos)); + elementCount = SkipFunctionIndexVector(bytes, ref pos); + break; + case 3: + mode = "declarative"; + elementType = ElementKindName(ReadByte(bytes, ref pos)); + elementCount = SkipFunctionIndexVector(bytes, ref pos); + break; + case 4: + mode = "active"; + tableIndex = 0; + SkipConstExpr(bytes, ref pos, end); + elementType = "funcref"; + elementCount = SkipExpressionVector(bytes, ref pos, end); + break; + case 5: + mode = "passive"; + elementType = ReadRefTypeName(bytes, ref pos); + elementCount = SkipExpressionVector(bytes, ref pos, end); + break; + case 6: + mode = "active-explicit-table"; + tableIndex = checked((int)ReadUleb(bytes, ref pos)); + SkipConstExpr(bytes, ref pos, end); + elementType = ReadRefTypeName(bytes, ref pos); + elementCount = SkipExpressionVector(bytes, ref pos, end); + break; + case 7: + mode = "declarative"; + elementType = ReadRefTypeName(bytes, ref pos); + elementCount = SkipExpressionVector(bytes, ref pos, end); + break; + default: + throw new InvalidDataException($"Unsupported WebAssembly element segment flags {flags}."); + } + + result.Add(new WasmElementSegmentInfo(i, mode, tableIndex, elementType, elementCount)); + } + + RequireSectionConsumed(pos, end); + return result; + } + + private static List ReadCodeSection( + ReadOnlySpan bytes, ref int pos, int end, int sectionPayloadOffset) + { + _ = sectionPayloadOffset; + var count = checked((int)ReadUleb(bytes, ref pos)); + var result = new List(count); + for (var i = 0; i < count; i++) + { + var bodySize = checked((int)ReadUleb(bytes, ref pos)); + var bodyOffset = pos; + var bodyEnd = checked(pos + bodySize); + if (bodyEnd > end) + throw new InvalidDataException("A WebAssembly function body extends past the code section."); + + var localCount = checked((int)ReadUleb(bytes, ref pos)); + var locals = new List(localCount); + for (var l = 0; l < localCount; l++) + { + var localRunCount = ReadUleb(bytes, ref pos); + var valueType = ReadByte(bytes, ref pos); + locals.Add(new WasmLocalInfo((uint)localRunCount, valueType, ValueTypeName(valueType))); + } + + var codeOffset = pos; + result.Add(new WasmFunctionBody(bodyOffset, bodySize, codeOffset, bodyEnd - codeOffset, locals)); + pos = bodyEnd; + } + + RequireSectionConsumed(pos, end); + return result; + } + + private static int ReadDataCountSection(ReadOnlySpan bytes, ref int pos, int end) + { + var count = checked((int)ReadUleb(bytes, ref pos)); + RequireSectionConsumed(pos, end); + return count; + } + + private static List ReadTagSection( + ReadOnlySpan bytes, ref int pos, int end, int importCount) + { + var count = checked((int)ReadUleb(bytes, ref pos)); + var result = new List(count); + for (var i = 0; i < count; i++) + { + var attribute = checked((uint)ReadUleb(bytes, ref pos)); + var typeIndex = checked((int)ReadUleb(bytes, ref pos)); + result.Add(new WasmTagInfo(importCount + i, attribute, typeIndex)); + } + + RequireSectionConsumed(pos, end); + return result; + } + + private static List ReadDataSection(ReadOnlySpan bytes, ref int pos, int end) + { + var count = checked((int)ReadUleb(bytes, ref pos)); + var result = new List(count); + for (var i = 0; i < count; i++) + { + var modeValue = checked((int)ReadUleb(bytes, ref pos)); + var mode = modeValue switch + { + 0 => "active", + 1 => "passive", + 2 => "active-explicit-memory", + _ => $"mode-{modeValue}", + }; + + if (modeValue == 0) + SkipConstExpr(bytes, ref pos, end); + else if (modeValue == 2) + { + _ = ReadUleb(bytes, ref pos); + SkipConstExpr(bytes, ref pos, end); + } + else if (modeValue != 1) + { + throw new InvalidDataException($"Unsupported WebAssembly data segment mode {modeValue}."); + } + + var size = checked((int)ReadUleb(bytes, ref pos)); + if (pos + size > end) + throw new InvalidDataException("A WebAssembly data segment extends past the data section."); + result.Add(new WasmDataSegmentInfo(i, mode, pos, size)); + pos += size; + } + + RequireSectionConsumed(pos, end); + return result; + } + + private static void ParseCustomSection( + string name, + ReadOnlySpan bytes, + int pos, + int end, + Dictionary functionNames, + List targetFeatures, + List producers) + { + try + { + if (name == "name") + ParseNameSection(bytes, pos, end, functionNames); + else if (name == "target_features") + ParseTargetFeatures(bytes, pos, end, targetFeatures); + else if (name == "producers") + ParseProducers(bytes, pos, end, producers); + } + catch (Exception ex) when (ex is InvalidDataException or OverflowException or ArgumentOutOfRangeException) + { + // Custom sections are descriptive. A corrupt custom payload should not hide code bodies. + } + } + + private static void ParseNameSection(ReadOnlySpan bytes, int pos, int end, Dictionary functionNames) + { + while (pos < end) + { + var subSectionId = ReadByte(bytes, ref pos); + var size = checked((int)ReadUleb(bytes, ref pos)); + var subEnd = checked(pos + size); + if (subEnd > end) + throw new InvalidDataException("A WebAssembly name subsection extends past the custom section."); + + if (subSectionId == 1) + { + var count = checked((int)ReadUleb(bytes, ref pos)); + for (var i = 0; i < count; i++) + { + var index = checked((int)ReadUleb(bytes, ref pos)); + var name = ReadName(bytes, ref pos, subEnd); + functionNames[index] = name; + } + } + + pos = subEnd; + } + } + + private static void ParseTargetFeatures(ReadOnlySpan bytes, int pos, int end, List targetFeatures) + { + var count = checked((int)ReadUleb(bytes, ref pos)); + for (var i = 0; i < count && pos < end; i++) + { + var prefix = ReadByte(bytes, ref pos); + var feature = ReadName(bytes, ref pos, end); + targetFeatures.Add($"{(char)prefix}{feature}"); + } + } + + private static void ParseProducers(ReadOnlySpan bytes, int pos, int end, List producers) + { + var fieldCount = checked((int)ReadUleb(bytes, ref pos)); + for (var f = 0; f < fieldCount && pos < end; f++) + { + var fieldName = ReadName(bytes, ref pos, end); + var valueCount = checked((int)ReadUleb(bytes, ref pos)); + for (var i = 0; i < valueCount && pos < end; i++) + { + var name = ReadName(bytes, ref pos, end); + var version = ReadName(bytes, ref pos, end); + producers.Add($"{fieldName}: {name} {version}".TrimEnd()); + } + } + } + + private static List BuildFunctions( + IReadOnlyList types, + List functionImports, + List functionTypeIndices, + IReadOnlyList bodies, + IReadOnlyList exports, + IReadOnlyDictionary nameSection, + IReadOnlyDictionary symbolMap) + { + var exportNames = exports + .Where(e => e.Kind == WasmExternalKind.Function) + .GroupBy(e => e.Index) + .ToDictionary(g => g.Key, g => (IReadOnlyList)[.. g.Select(e => e.Name).Order(StringComparer.Ordinal)]); + + var functions = new List(functionImports.Count + bodies.Count); + for (var i = 0; i < functionImports.Count; i++) + { + var import = functionImports[i]; + var (name, source) = BestName(i, symbolMap, nameSection, exportNames, $"{import.ModuleName}!{import.Name}", "import"); + var type = TypeFor(types, import.TypeIndex); + functions.Add(new WasmFunctionInfo( + Index: i, + TypeIndex: import.TypeIndex, + Name: name, + NameSource: source, + IsImported: true, + ImportModule: import.ModuleName, + ImportName: import.Name, + IsExported: exportNames.ContainsKey(i), + ExportNames: exportNames.GetValueOrDefault(i, []), + BodyOffset: null, + BodySize: 0, + CodeOffset: null, + CodeSize: 0, + Locals: [], + ParamTypes: type.ParamTypes, + ResultTypes: type.ResultTypes)); + } + + for (var i = 0; i < bodies.Count; i++) + { + var index = functionImports.Count + i; + var typeIndex = i < functionTypeIndices.Count ? functionTypeIndices[i] : (int?)null; + var type = TypeFor(types, typeIndex); + var body = bodies[i]; + var (name, source) = BestName(index, symbolMap, nameSection, exportNames, $"func_{index}", "synthetic"); + functions.Add(new WasmFunctionInfo( + Index: index, + TypeIndex: typeIndex, + Name: name, + NameSource: source, + IsImported: false, + ImportModule: null, + ImportName: null, + IsExported: exportNames.ContainsKey(index), + ExportNames: exportNames.GetValueOrDefault(index, []), + BodyOffset: body.BodyOffset, + BodySize: body.BodySize, + CodeOffset: body.CodeOffset, + CodeSize: body.CodeSize, + Locals: body.Locals, + ParamTypes: type.ParamTypes, + ResultTypes: type.ResultTypes)); + } + + return functions; + } + + private static (string Name, string Source) BestName( + int index, + IReadOnlyDictionary symbolMap, + IReadOnlyDictionary nameSection, + Dictionary> exportNames, + string fallback, + string fallbackSource) + { + if (symbolMap.TryGetValue(index, out var symbol) && !string.IsNullOrWhiteSpace(symbol)) + return (symbol, "symbol-map"); + if (nameSection.TryGetValue(index, out var name) && !string.IsNullOrWhiteSpace(name)) + return (name, "name-section"); + if (exportNames.TryGetValue(index, out var exports) && exports.Count > 0) + return (exports[0], "export"); + return (fallback, fallbackSource); + } + + private static WasmTypeInfo TypeFor(IReadOnlyList types, int? typeIndex) => + typeIndex is { } index && index >= 0 && index < types.Count + ? types[index] + : new WasmTypeInfo(-1, [], []); + + private static (string? Path, WasmSymbolMapStatus Status, Dictionary Entries) ReadSymbolMap(string? filePath) + { + if (string.IsNullOrEmpty(filePath)) + return (null, WasmSymbolMapStatus.Missing, []); + + var dir = Path.GetDirectoryName(filePath); + if (string.IsNullOrEmpty(dir)) + return (null, WasmSymbolMapStatus.Missing, []); + + var stem = Path.GetFileNameWithoutExtension(filePath); + var candidates = new[] + { + Path.Combine(dir, stem + ".js.symbols"), + filePath + ".symbols", + Path.Combine(dir, stem + ".symbols"), + }; + + var path = candidates.FirstOrDefault(File.Exists); + if (path is null) + return (null, WasmSymbolMapStatus.Missing, []); + + var entries = new Dictionary(); + try + { + foreach (var line in File.ReadLines(path)) + { + var colon = line.IndexOf(':'); + if (colon <= 0) + continue; + if (!int.TryParse(line[..colon], NumberStyles.Integer, CultureInfo.InvariantCulture, out var index)) + continue; + var name = line[(colon + 1)..].Trim(); + if (name.Length > 0) + entries[index] = name; + } + } + catch (IOException) + { + return (path, WasmSymbolMapStatus.Corrupt, []); + } + + return entries.Count > 0 + ? (path, WasmSymbolMapStatus.Loaded, entries) + : (path, WasmSymbolMapStatus.Corrupt, entries); + } + + private static bool IsWebcilPayload(ReadOnlySpan bytes) + { + if (bytes.Length < 28) + return false; + + var magic = BinaryPrimitives.ReadUInt32LittleEndian(bytes); + var major = BinaryPrimitives.ReadUInt16LittleEndian(bytes[4..]); + var minor = BinaryPrimitives.ReadUInt16LittleEndian(bytes[6..]); + return magic == WebcilMagic && major is 0 or 1 && minor == 0; + } + + private static void SkipTableType(ReadOnlySpan bytes, ref int pos) + { + _ = ReadRefTypeName(bytes, ref pos); + SkipLimits(bytes, ref pos); + } + + private static void SkipLimits(ReadOnlySpan bytes, ref int pos) + { + _ = ReadLimits(bytes, ref pos); + } + + private static (ulong Minimum, ulong? Maximum, bool IsShared, bool IsMemory64) ReadLimits( + ReadOnlySpan bytes, ref int pos) + { + var flags = ReadUleb(bytes, ref pos); + var minimum = ReadUleb(bytes, ref pos); + ulong? maximum = null; + if ((flags & 0x01) != 0) + maximum = ReadUleb(bytes, ref pos); + + return (minimum, maximum, (flags & 0x02) != 0, (flags & 0x04) != 0); + } + + private static string ReadRefTypeName(ReadOnlySpan bytes, ref int pos) + { + var first = ReadByte(bytes, ref pos); + if (first is 0x63 or 0x64) + { + var heapType = ReadSleb(bytes, ref pos); + return first == 0x63 ? $"ref null {HeapTypeName(heapType)}" : $"ref {HeapTypeName(heapType)}"; + } + + return RefTypeName(first); + } + + private static int SkipFunctionIndexVector(ReadOnlySpan bytes, ref int pos) + { + var count = checked((int)ReadUleb(bytes, ref pos)); + for (var i = 0; i < count; i++) + _ = ReadUleb(bytes, ref pos); + return count; + } + + private static int SkipExpressionVector(ReadOnlySpan bytes, ref int pos, int end) + { + var count = checked((int)ReadUleb(bytes, ref pos)); + for (var i = 0; i < count; i++) + SkipConstExpr(bytes, ref pos, end); + return count; + } + + private static string ElementKindName(byte elementKind) => elementKind switch + { + 0x00 => "funcref", + _ => $"0x{elementKind:X2}", + }; + + private static string RefTypeName(byte refType) => refType switch + { + 0x70 => "funcref", + 0x6F => "externref", + 0x6E => "anyref", + 0x6D => "eqref", + 0x6C => "i31ref", + 0x6B => "structref", + 0x6A => "arrayref", + 0x69 => "exnref", + 0x68 => "stringref", + 0x67 => "stringview_wtf8", + 0x66 => "stringview_wtf16", + 0x65 => "stringview_iter", + 0x64 => "ref", + 0x63 => "ref null", + _ => $"0x{refType:X2}", + }; + + private static string HeapTypeName(long heapType) => heapType switch + { + -0x10 => "func", + -0x11 => "extern", + -0x12 => "any", + -0x13 => "eq", + -0x14 => "i31", + -0x15 => "struct", + -0x16 => "array", + -0x17 => "none", + -0x18 => "nofunc", + -0x19 => "noextern", + -0x1A => "exn", + -0x1B => "noexn", + _ => heapType.ToString(CultureInfo.InvariantCulture), + }; + + private static void SkipRefType(ReadOnlySpan bytes, ref int pos) + { + var first = ReadByte(bytes, ref pos); + if (first is 0x63 or 0x64) + _ = ReadSleb(bytes, ref pos); + } + + private static void SkipConstExpr(ReadOnlySpan bytes, ref int pos, int end) + { + while (pos < end) + { + var op = ReadByte(bytes, ref pos); + if (op == 0x0B) + return; + + switch (op) + { + case 0x41: + case 0x42: + _ = ReadSleb(bytes, ref pos); + break; + case 0x43: + pos = checked(pos + 4); + break; + case 0x44: + pos = checked(pos + 8); + break; + case 0x23: + case 0xD0: + case 0xD2: + _ = ReadUleb(bytes, ref pos); + break; + } + } + + throw new InvalidDataException("A WebAssembly constant expression is unterminated."); + } + + private static string ReadName(ReadOnlySpan bytes, ref int pos, int end) + { + var length = checked((int)ReadUleb(bytes, ref pos)); + if (pos + length > end) + throw new InvalidDataException("A WebAssembly name extends past its containing section."); + + var name = Encoding.UTF8.GetString(bytes.Slice(pos, length)); + pos += length; + return name; + } + + private static byte ReadByte(ReadOnlySpan bytes, ref int pos) + { + if ((uint)pos >= (uint)bytes.Length) + throw new InvalidDataException("Unexpected end of WebAssembly data."); + + return bytes[pos++]; + } + + private static ulong ReadUleb(ReadOnlySpan bytes, ref int pos) + { + ulong value = 0; + var shift = 0; + while (true) + { + var b = ReadByte(bytes, ref pos); + value |= (ulong)(b & 0x7F) << shift; + if ((b & 0x80) == 0) + return value; + + shift += 7; + if (shift >= 64) + throw new InvalidDataException("A WebAssembly LEB128 value is too large."); + } + } + + private static long ReadSleb(ReadOnlySpan bytes, ref int pos) + { + long value = 0; + var shift = 0; + byte b; + do + { + b = ReadByte(bytes, ref pos); + value |= (long)(b & 0x7F) << shift; + shift += 7; + } while ((b & 0x80) != 0 && shift < 64); + + if (shift < 64 && (b & 0x40) != 0) + value |= -1L << shift; + + return value; + } + + private static void RequireSectionConsumed(int pos, int end) + { + if (pos != end) + throw new InvalidDataException("A WebAssembly section was not consumed exactly."); + } + + private static WasmExternalKind ExternalKind(byte kind) => kind switch + { + 0 => WasmExternalKind.Function, + 1 => WasmExternalKind.Table, + 2 => WasmExternalKind.Memory, + 3 => WasmExternalKind.Global, + 4 => WasmExternalKind.Tag, + _ => WasmExternalKind.Unknown, + }; + + private static string StandardSectionName(byte id) => id switch + { + 0 => "custom", + 1 => "type", + 2 => "import", + 3 => "function", + 4 => "table", + 5 => "memory", + 6 => "global", + 7 => "export", + 8 => "start", + 9 => "element", + 10 => "code", + 11 => "data", + 12 => "data-count", + 13 => "tag", + _ => $"section-{id}", + }; + + private static string ValueTypeName(byte valueType) => valueType switch + { + 0x7F => "i32", + 0x7E => "i64", + 0x7D => "f32", + 0x7C => "f64", + 0x7B => "v128", + 0x70 => "funcref", + 0x6F => "externref", + _ => $"0x{valueType:X2}", + }; + + private readonly record struct WasmFunctionBody( + int BodyOffset, + int BodySize, + int CodeOffset, + int CodeSize, + IReadOnlyList Locals); +} diff --git a/src/Dotsider.Core/Analysis/Wasm/WasmSymbolBuilder.cs b/src/Dotsider.Core/Analysis/Wasm/WasmSymbolBuilder.cs new file mode 100644 index 00000000..cefa2c2a --- /dev/null +++ b/src/Dotsider.Core/Analysis/Wasm/WasmSymbolBuilder.cs @@ -0,0 +1,61 @@ +using Dotsider.Core.Analysis.Models; + +namespace Dotsider.Core.Analysis.Wasm; + +/// +/// Projects a parsed WebAssembly module into native-symbol records for shared disassembly surfaces. +/// +internal static class WasmSymbolBuilder +{ + /// + /// Builds native symbols for every defined, file-backed Wasm function. + /// + /// The parsed WebAssembly module. + /// A native-symbol info object using WebAssembly provenance and Wasm32 architecture. + public static NativeSymbolInfo Build(WasmModuleInfo module) + { + var symbols = module.Functions + .Where(static f => !f.IsImported && f.CodeOffset is not null && f.CodeSize > 0) + .Select(static f => + { + var aliases = f.ExportNames + .Where(name => !string.Equals(name, f.Name, StringComparison.Ordinal)) + .Distinct(StringComparer.Ordinal) + .ToList(); + aliases.Add($"func:{f.Index}"); + aliases.Add($"func_{f.Index}"); + + return new NativeSymbol( + Name: f.Name, + ManagedName: null, + VirtualAddress: (ulong)f.CodeOffset!.Value, + Rva: null, + FileOffset: f.CodeOffset, + Section: "code", + Size: f.CodeSize, + Kind: NativeSymbolKind.Function, + SourceFile: null, + Line: null, + IsExactMatch: true, + Aliases: aliases); + }) + .OrderBy(static s => s.VirtualAddress) + .ToList(); + + var diagnostic = module.SymbolMapStatus switch + { + WasmSymbolMapStatus.Loaded => null, + WasmSymbolMapStatus.Corrupt => $"symbol map '{module.SymbolMapPath}' could not be parsed", + _ => "dotnet.native.js.symbols not found; using Wasm name/export/synthetic names", + }; + + return new NativeSymbolInfo( + symbols, + NativeSymbolSource.WebAssembly, + symbols.Count > 0 ? NativeSymbolStatus.Loaded : NativeSymbolStatus.NoSymbolFile, + module.SymbolMapPath, + diagnostic, + NativeArchitecture.Wasm32, + SourceMap: null); + } +} diff --git a/src/Dotsider.Core/Analysis/Wasm/WebcilCodeViewData.cs b/src/Dotsider.Core/Analysis/Wasm/WebcilCodeViewData.cs new file mode 100644 index 00000000..324efb68 --- /dev/null +++ b/src/Dotsider.Core/Analysis/Wasm/WebcilCodeViewData.cs @@ -0,0 +1,11 @@ +namespace Dotsider.Core.Analysis.Wasm; + +/// +/// Carries CodeView portable-PDB identity decoded from a Webcil debug entry. +/// The GUID and age match the sidecar PDB identity that System.Reflection.Metadata expects. +/// The path is the original build-time PDB path and is only used to derive local sidecar probes. +/// +/// The portable PDB signature GUID. +/// The CodeView age value. +/// The build-time PDB path recorded in the CodeView payload. +internal readonly record struct WebcilCodeViewData(Guid Guid, int Age, string Path); diff --git a/src/Dotsider.Core/Analysis/Wasm/WebcilDebugEntry.cs b/src/Dotsider.Core/Analysis/Wasm/WebcilDebugEntry.cs new file mode 100644 index 00000000..25f7cc42 --- /dev/null +++ b/src/Dotsider.Core/Analysis/Wasm/WebcilDebugEntry.cs @@ -0,0 +1,24 @@ +using System.Reflection.PortableExecutable; + +namespace Dotsider.Core.Analysis.Wasm; + +/// +/// Describes one debug-directory entry from a Webcil image. +/// The values are stored in Webcil-relative form and translated by the reader when displayed. +/// Dotsider uses these entries to recover portable-PDB and Source Link data from Webcil assemblies. +/// +/// The debug directory timestamp or content id value. +/// The debug entry major version. +/// The debug entry minor version. +/// The debug directory entry type. +/// The payload size in bytes. +/// The payload RVA recorded by the Webcil debug entry. +/// The Webcil-relative payload pointer. +internal readonly record struct WebcilDebugEntry( + uint Stamp, + ushort MajorVersion, + ushort MinorVersion, + DebugDirectoryEntryType Type, + int DataSize, + int DataRva, + int DataPointer); diff --git a/src/Dotsider.Core/Analysis/Wasm/WebcilImageReader.cs b/src/Dotsider.Core/Analysis/Wasm/WebcilImageReader.cs new file mode 100644 index 00000000..7e288408 --- /dev/null +++ b/src/Dotsider.Core/Analysis/Wasm/WebcilImageReader.cs @@ -0,0 +1,602 @@ +using Dotsider.Core.Analysis.Models; +using System.Buffers.Binary; +using System.Collections.Immutable; +using System.IO.Compression; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; + +namespace Dotsider.Core.Analysis.Wasm; + +/// +/// Reads Webcil managed assemblies, including Webcil payloads wrapped inside WebAssembly modules. +/// This mirrors the runtime Webcil reader closely enough for dotsider to expose metadata, IL, +/// debug-directory, and portable-PDB behavior without depending on runtime tooling. +/// +internal sealed class WebcilImageReader +{ + private const uint WebcilMagic = 0x4C496257; + private const uint WasmMagic = 0x6D736100; + private const uint WasmVersion = 1; + private const int HeaderV0Size = 28; + private const int HeaderV1Size = 32; + private const int SectionHeaderSize = 16; + private const int DebugDirectoryEntrySize = 28; + private const uint EmbeddedPortablePdbSignature = 0x4244504D; + + private readonly byte[] _image; + private readonly List _sections; + private readonly List _debugEntries; + private readonly WebcilHeader _header; + private readonly byte[] _metadataBytes; + + private WebcilImageReader( + byte[] image, + long payloadOffset, + WebcilHeader header, + List sections, + byte[] metadataBytes, + List debugEntries) + { + _image = image; + PayloadOffset = payloadOffset; + _header = header; + _sections = sections; + _metadataBytes = metadataBytes; + _debugEntries = debugEntries; + Info = new WebcilInfo( + header.VersionMajor, + header.VersionMinor, + payloadOffset > 0, + payloadOffset, + sections.Count, + metadataBytes.Length, + checked((int)header.PeDebugSize)); + ClrHeader = ReadClrHeader(header, sections, image, payloadOffset); + } + + /// + /// Gets display and provenance information for the Webcil image. + /// + public WebcilInfo Info { get; } + + /// + /// Gets the CLR header reconstructed from the Webcil COR directory. + /// + public ClrHeader ClrHeader { get; } + + /// + /// Attempts to read a Webcil image from raw bytes. + /// + /// The candidate file bytes. + /// The parsed Webcil reader when the bytes contain Webcil. + /// True when a bare or Wasm-wrapped Webcil payload was found and parsed. + public static bool TryRead(ReadOnlySpan bytes, out WebcilImageReader? reader) + { + reader = null; + if (!TryFindPayload(bytes, out var payloadOffset)) + return false; + + try + { + if (!TryReadHeader(bytes, payloadOffset, out var header)) + return false; + + var sections = ReadSections(bytes, payloadOffset, header); + var metadataBytes = ReadMetadata(bytes, payloadOffset, header, sections); + var debugEntries = ReadDebugEntries(bytes, payloadOffset, header, sections); + reader = new WebcilImageReader( + bytes.ToArray(), + payloadOffset, + header, + sections, + metadataBytes, + debugEntries); + return true; + } + catch (Exception ex) when (ex is BadImageFormatException or ArgumentOutOfRangeException or OverflowException) + { + reader = null; + return false; + } + } + + /// + /// Creates a metadata provider over the Webcil metadata blob. + /// + /// A metadata provider over the Webcil ECMA-335 metadata image. + public MetadataReaderProvider CreateMetadataReaderProvider() => + MetadataReaderProvider.FromMetadataImage(ImmutableArray.Create(_metadataBytes)); + + /// + /// Reads a managed method body by RVA from the Webcil section table. + /// + /// The method body's RVA. + /// The method body block, or null when the RVA does not map to Webcil bytes. + public unsafe MethodBodyBlock? GetMethodBody(int rva) + { + if (rva == 0 || !TryTranslateRva((uint)rva, out var offset, out var available) || available <= 0) + return null; + + fixed (byte* ptr = &_image[(int)offset]) + { + var blob = new BlobReader(ptr, checked((int)available)); + return MethodBodyBlock.Create(blob); + } + } + + /// + /// Converts Webcil sections to dotsider's generic section table rows. + /// + /// Generic section rows with Webcil-adjusted raw data offsets. + public IReadOnlyList ReadSections() => + [.. _sections.Select((s, i) => new SectionInfo( + Name: $"webcil-section-{i}", + VirtualAddress: checked((int)s.VirtualAddress), + VirtualSize: checked((int)s.VirtualSize), + RawDataOffset: checked((int)(PayloadOffset + s.PointerToRawData)), + RawDataSize: checked((int)s.SizeOfRawData), + Characteristics: 0))]; + + /// + /// Converts Webcil debug directory entries to dotsider display rows. + /// + /// Debug directory rows with formatted Webcil payload details. + public IReadOnlyList ReadDebugDirectory() => + [.. _debugEntries.Select(e => new DebugDirectoryInfo( + Type: e.Type, + Stamp: e.Stamp, + MajorVersion: e.MajorVersion, + MinorVersion: e.MinorVersion, + DataSize: e.DataSize, + AddressOfRawData: e.DataRva, + PointerToRawData: checked((int)(PayloadOffset + e.DataPointer)), + Payload: FormatPayload(e)))]; + + /// + /// Opens an embedded portable PDB from a Webcil debug directory entry. + /// + /// The embedded portable PDB debug directory entry. + /// A metadata provider over the decompressed portable PDB image. + public MetadataReaderProvider ReadEmbeddedPortablePdb(WebcilDebugEntry entry) + { + var payload = ReadEntryPayload(entry); + if (payload.Length < 8 || BinaryPrimitives.ReadUInt32LittleEndian(payload) != EmbeddedPortablePdbSignature) + throw new BadImageFormatException("Unexpected embedded portable PDB signature."); + + var decompressedSize = BinaryPrimitives.ReadInt32LittleEndian(payload[4..]); + using var compressed = new MemoryStream(payload[8..].ToArray(), writable: false); + using var deflate = new DeflateStream(compressed, CompressionMode.Decompress); + var decompressed = new byte[decompressedSize]; + deflate.ReadExactly(decompressed); + return MetadataReaderProvider.FromPortablePdbImage(ImmutableArray.Create(decompressed)); + } + + /// + /// Gets the first embedded portable PDB entry from the Webcil debug directory. + /// + /// The embedded portable PDB entry, or null when absent. + public WebcilDebugEntry? EmbeddedPortablePdbEntry() => + FindDebugEntry(DebugDirectoryEntryType.EmbeddedPortablePdb); + + /// + /// Gets the first CodeView entry from the Webcil debug directory. + /// + /// The CodeView debug entry, or null when absent. + public WebcilDebugEntry? CodeViewEntry() => + FindDebugEntry(DebugDirectoryEntryType.CodeView); + + /// + /// Decodes CodeView portable-PDB identity data from a Webcil debug entry. + /// + /// The CodeView debug directory entry. + /// The portable PDB identity and build-time path. + public WebcilCodeViewData ReadCodeView(WebcilDebugEntry entry) + { + var payload = ReadEntryPayload(entry); + if (payload.Length < 24 + || payload[0] != (byte)'R' + || payload[1] != (byte)'S' + || payload[2] != (byte)'D' + || payload[3] != (byte)'S') + { + throw new BadImageFormatException("Unexpected CodeView payload signature."); + } + + var guid = new Guid(payload[4..20]); + var age = BinaryPrimitives.ReadInt32LittleEndian(payload[20..]); + var path = ReadUtf8NullTerminated(payload[24..]); + return new WebcilCodeViewData(guid, age, path); + } + + /// + /// Reads a 32-bit value at the requested RVA. + /// + /// The base RVA to translate through the Webcil section table. + /// The byte offset from . + /// The decoded little-endian 32-bit value. + /// True when the requested bytes mapped to the Webcil image. + public bool TryReadInt32AtRva(int rva, int relativeOffset, out int value) + { + value = 0; + if (!TryTranslateRva((uint)rva, out var offset, out var available) + || relativeOffset < 0 + || relativeOffset + 4 > available) + { + return false; + } + + value = BinaryPrimitives.ReadInt32LittleEndian(_image.AsSpan((int)offset + relativeOffset, 4)); + return true; + } + + private long PayloadOffset { get; } + + private static bool TryFindPayload(ReadOnlySpan bytes, out long payloadOffset) + { + payloadOffset = 0; + if (bytes.Length < 4) + return false; + + if (BinaryPrimitives.ReadUInt32LittleEndian(bytes) == WebcilMagic) + return true; + + if (bytes.Length < 8 + || BinaryPrimitives.ReadUInt32LittleEndian(bytes) != WasmMagic + || BinaryPrimitives.ReadUInt32LittleEndian(bytes[4..]) != WasmVersion) + { + return false; + } + + var pos = 8; + while (pos < bytes.Length) + { + var sectionId = ReadByte(bytes, ref pos); + var sectionSize = checked((int)ReadUleb(bytes, ref pos)); + var sectionEnd = checked(pos + sectionSize); + if (sectionEnd > bytes.Length) + return false; + + if (sectionId != 11) + { + pos = sectionEnd; + continue; + } + + var segmentCount = checked((int)ReadUleb(bytes, ref pos)); + for (var i = 0; i < segmentCount && pos < sectionEnd; i++) + { + var mode = ReadByte(bytes, ref pos); + if (mode == 0) + SkipConstExpr(bytes, ref pos, sectionEnd); + else if (mode == 2) + { + _ = ReadUleb(bytes, ref pos); + SkipConstExpr(bytes, ref pos, sectionEnd); + } + else if (mode != 1) + { + return false; + } + + var size = checked((int)ReadUleb(bytes, ref pos)); + if (pos + size > sectionEnd) + return false; + + var segment = bytes[pos..(pos + size)]; + if (segment.Length >= 4 + && BinaryPrimitives.ReadUInt32LittleEndian(segment) == WebcilMagic + && TryReadHeader(bytes, pos, out _)) + { + payloadOffset = pos; + return true; + } + + pos += size; + } + + return false; + } + + return false; + } + + private static bool TryReadHeader(ReadOnlySpan bytes, long offset, out WebcilHeader header) + { + header = default; + if (offset < 0 || offset + HeaderV0Size > bytes.Length) + return false; + + var span = bytes[(int)offset..]; + header = new WebcilHeader( + Id: BinaryPrimitives.ReadUInt32LittleEndian(span), + VersionMajor: BinaryPrimitives.ReadUInt16LittleEndian(span[4..]), + VersionMinor: BinaryPrimitives.ReadUInt16LittleEndian(span[6..]), + CoffSections: BinaryPrimitives.ReadUInt16LittleEndian(span[8..]), + PeCliHeaderRva: BinaryPrimitives.ReadUInt32LittleEndian(span[12..]), + PeCliHeaderSize: BinaryPrimitives.ReadUInt32LittleEndian(span[16..]), + PeDebugRva: BinaryPrimitives.ReadUInt32LittleEndian(span[20..]), + PeDebugSize: BinaryPrimitives.ReadUInt32LittleEndian(span[24..]), + TableBase: uint.MaxValue); + + if (header.Id != WebcilMagic + || header.VersionMajor is not (0 or 1) + || header.VersionMinor != 0) + { + return false; + } + + if (header.VersionMajor >= 1) + { + if (offset + HeaderV1Size > bytes.Length) + return false; + header = header with { TableBase = BinaryPrimitives.ReadUInt32LittleEndian(span[HeaderV0Size..]) }; + } + + return true; + } + + private static List ReadSections(ReadOnlySpan bytes, long payloadOffset, WebcilHeader header) + { + var sectionOffset = checked(payloadOffset + (header.VersionMajor >= 1 ? HeaderV1Size : HeaderV0Size)); + var result = new List(header.CoffSections); + for (var i = 0; i < header.CoffSections; i++) + { + var offset = checked(sectionOffset + i * SectionHeaderSize); + if (offset + SectionHeaderSize > bytes.Length) + throw new BadImageFormatException("The Webcil section table is truncated."); + + var span = bytes[(int)offset..]; + result.Add(new WebcilSection( + VirtualSize: BinaryPrimitives.ReadUInt32LittleEndian(span), + VirtualAddress: BinaryPrimitives.ReadUInt32LittleEndian(span[4..]), + SizeOfRawData: BinaryPrimitives.ReadUInt32LittleEndian(span[8..]), + PointerToRawData: BinaryPrimitives.ReadUInt32LittleEndian(span[12..]))); + } + + return result; + } + + private static byte[] ReadMetadata( + ReadOnlySpan bytes, + long payloadOffset, + WebcilHeader header, + IReadOnlyList sections) + { + var clr = ReadClrHeader(header, sections, bytes, payloadOffset); + if (!TryTranslateRva(sections, payloadOffset, (uint)clr.MetadataRva, out var offset, out var available) + || clr.MetadataSize <= 0 + || clr.MetadataSize > available) + { + throw new BadImageFormatException("The Webcil metadata directory does not map to file bytes."); + } + + return bytes.Slice((int)offset, clr.MetadataSize).ToArray(); + } + + private static List ReadDebugEntries( + ReadOnlySpan bytes, + long payloadOffset, + WebcilHeader header, + IReadOnlyList sections) + { + if (header.PeDebugRva == 0 || header.PeDebugSize == 0) + return []; + + if (!TryTranslateRva(sections, payloadOffset, header.PeDebugRva, out var offset, out var available) + || header.PeDebugSize > available + || header.PeDebugSize % DebugDirectoryEntrySize != 0) + { + return []; + } + + var count = checked((int)header.PeDebugSize / DebugDirectoryEntrySize); + var result = new List(count); + for (var i = 0; i < count; i++) + { + var span = bytes.Slice((int)offset + i * DebugDirectoryEntrySize, DebugDirectoryEntrySize); + var characteristics = BinaryPrimitives.ReadUInt32LittleEndian(span); + if (characteristics != 0) + continue; + + result.Add(new WebcilDebugEntry( + Stamp: BinaryPrimitives.ReadUInt32LittleEndian(span[4..]), + MajorVersion: BinaryPrimitives.ReadUInt16LittleEndian(span[8..]), + MinorVersion: BinaryPrimitives.ReadUInt16LittleEndian(span[10..]), + Type: (DebugDirectoryEntryType)BinaryPrimitives.ReadInt32LittleEndian(span[12..]), + DataSize: BinaryPrimitives.ReadInt32LittleEndian(span[16..]), + DataRva: BinaryPrimitives.ReadInt32LittleEndian(span[20..]), + DataPointer: BinaryPrimitives.ReadInt32LittleEndian(span[24..]))); + } + + return result; + } + + private static ClrHeader ReadClrHeader( + WebcilHeader header, + IReadOnlyList sections, + ReadOnlySpan bytes, + long payloadOffset) + { + if (!TryTranslateRva(sections, payloadOffset, header.PeCliHeaderRva, out var offset, out var available) + || available < 72) + { + throw new BadImageFormatException("The Webcil CLR header does not map to file bytes."); + } + + var span = bytes[(int)offset..]; + return new ClrHeader( + MajorRuntimeVersion: BinaryPrimitives.ReadUInt16LittleEndian(span[4..]), + MinorRuntimeVersion: BinaryPrimitives.ReadUInt16LittleEndian(span[6..]), + MetadataRva: BinaryPrimitives.ReadInt32LittleEndian(span[8..]), + MetadataSize: BinaryPrimitives.ReadInt32LittleEndian(span[12..]), + Flags: (CorFlags)BinaryPrimitives.ReadUInt32LittleEndian(span[16..]), + EntryPointToken: BinaryPrimitives.ReadInt32LittleEndian(span[20..]), + ResourcesRva: BinaryPrimitives.ReadInt32LittleEndian(span[24..]), + ResourcesSize: BinaryPrimitives.ReadInt32LittleEndian(span[28..]), + StrongNameSignatureRva: BinaryPrimitives.ReadInt32LittleEndian(span[32..]), + StrongNameSignatureSize: BinaryPrimitives.ReadInt32LittleEndian(span[36..]), + ManagedNativeHeader: new DirectoryEntry( + BinaryPrimitives.ReadInt32LittleEndian(span[64..]), + BinaryPrimitives.ReadInt32LittleEndian(span[68..]))); + } + + private string FormatPayload(WebcilDebugEntry entry) + { + try + { + return entry.Type switch + { + DebugDirectoryEntryType.CodeView => FormatCodeViewPayload(entry), + DebugDirectoryEntryType.Reproducible => "present", + DebugDirectoryEntryType.PdbChecksum => FormatPdbChecksumPayload(entry), + DebugDirectoryEntryType.EmbeddedPortablePdb => FormatEmbeddedPortablePdbPayload(entry), + _ => "" + }; + } + catch (Exception ex) when (ex is BadImageFormatException or IOException or ArgumentOutOfRangeException) + { + return $"unreadable: {ex.Message}"; + } + } + + private string FormatCodeViewPayload(WebcilDebugEntry entry) + { + var data = ReadCodeView(entry); + return $"Portable PDB; PDB GUID: {data.Guid}; age: {data.Age}; path: {data.Path}"; + } + + private string FormatPdbChecksumPayload(WebcilDebugEntry entry) + { + var payload = ReadEntryPayload(entry); + var nul = payload.IndexOf((byte)0); + if (nul < 0) + return ""; + + var algorithm = System.Text.Encoding.UTF8.GetString(payload[..nul]); + var checksum = payload[(nul + 1)..]; + return $"Algorithm: {algorithm}; checksum: {Convert.ToHexString(checksum)}"; + } + + private string FormatEmbeddedPortablePdbPayload(WebcilDebugEntry entry) + { + var payload = ReadEntryPayload(entry); + return payload.Length >= 8 + ? $"present; uncompressed size: {BinaryPrimitives.ReadInt32LittleEndian(payload[4..])} bytes" + : "present"; + } + + private ReadOnlySpan ReadEntryPayload(WebcilDebugEntry entry) + { + var offset = checked(PayloadOffset + entry.DataPointer); + if (entry.DataSize < 0 || offset < 0 || offset + entry.DataSize > _image.Length) + throw new BadImageFormatException("The Webcil debug entry payload is out of range."); + return _image.AsSpan((int)offset, entry.DataSize); + } + + private bool TryTranslateRva(uint rva, out long offset, out long available) => + TryTranslateRva(_sections, PayloadOffset, rva, out offset, out available); + + private WebcilDebugEntry? FindDebugEntry(DebugDirectoryEntryType type) + { + foreach (var entry in _debugEntries) + if (entry.Type == type) + return entry; + + return null; + } + + private static bool TryTranslateRva( + IReadOnlyList sections, + long payloadOffset, + uint rva, + out long offset, + out long available) + { + offset = 0; + available = 0; + foreach (var section in sections) + { + if (rva < section.VirtualAddress || rva >= section.VirtualAddress + section.VirtualSize) + continue; + + var delta = rva - section.VirtualAddress; + if (delta >= section.SizeOfRawData) + return false; + + offset = checked(payloadOffset + section.PointerToRawData + delta); + available = section.SizeOfRawData - delta; + return true; + } + + return false; + } + + private static byte ReadByte(ReadOnlySpan bytes, ref int pos) + { + if ((uint)pos >= (uint)bytes.Length) + throw new BadImageFormatException("Unexpected end of WebAssembly data."); + return bytes[pos++]; + } + + private static uint ReadUleb(ReadOnlySpan bytes, ref int pos) + { + uint result = 0; + var shift = 0; + while (true) + { + var b = ReadByte(bytes, ref pos); + result |= (uint)(b & 0x7F) << shift; + if ((b & 0x80) == 0) + return result; + shift += 7; + if (shift >= 35) + throw new BadImageFormatException("WebAssembly ULEB128 value is too large."); + } + } + + private static void SkipConstExpr(ReadOnlySpan bytes, ref int pos, int end) + { + while (pos < end) + { + var opcode = ReadByte(bytes, ref pos); + switch (opcode) + { + case 0x0B: + return; + case 0x41: + case 0x42: + _ = ReadUleb(bytes, ref pos); + break; + case 0x23: + _ = ReadUleb(bytes, ref pos); + break; + default: + break; + } + } + } + + private static string ReadUtf8NullTerminated(ReadOnlySpan bytes) + { + var nul = bytes.IndexOf((byte)0); + return System.Text.Encoding.UTF8.GetString(nul >= 0 ? bytes[..nul] : bytes); + } + + private readonly record struct WebcilHeader( + uint Id, + int VersionMajor, + int VersionMinor, + int CoffSections, + uint PeCliHeaderRva, + uint PeCliHeaderSize, + uint PeDebugRva, + uint PeDebugSize, + uint TableBase); + + private readonly record struct WebcilSection( + uint VirtualSize, + uint VirtualAddress, + uint SizeOfRawData, + uint PointerToRawData); + +} diff --git a/src/Dotsider.Core/Protocol/WasmPayloadBuilder.cs b/src/Dotsider.Core/Protocol/WasmPayloadBuilder.cs new file mode 100644 index 00000000..ec49f025 --- /dev/null +++ b/src/Dotsider.Core/Protocol/WasmPayloadBuilder.cs @@ -0,0 +1,132 @@ +using Dotsider.Core.Analysis; +using Dotsider.Core.Analysis.Models; + +namespace Dotsider.Core.Protocol; + +/// +/// Builds JSON-ready WebAssembly payloads shared by direct MCP tools, the CLI, and the +/// diagnostics session protocol. The payloads describe raw SDK-produced Wasm modules such as +/// dotnet.native.wasm, not ECMA-335 metadata assemblies. +/// +public static class WasmPayloadBuilder +{ + /// + /// Builds a compact WebAssembly module summary for protocol surfaces. Returns null when the + /// analyzer is not a raw Wasm module so callers can include it unconditionally beside other + /// binary-kind summaries. + /// + /// The analyzer whose raw Wasm summary should be serialized. + /// A JSON-ready summary object, or null when the analyzer is not raw Wasm. + public static object? BuildSummary(AssemblyAnalyzer analyzer) + { + if (analyzer.WasmModuleInfo is not { } wasm) + return null; + + return new + { + wasm.Version, + SectionCount = wasm.Sections.Count, + TypeCount = wasm.Types.Count, + ImportCount = wasm.Imports.Count, + ExportCount = wasm.Exports.Count, + FunctionCount = wasm.Functions.Count, + wasm.ImportedFunctionCount, + wasm.DefinedFunctionCount, + wasm.CodeSize, + TableCount = wasm.Tables.Count, + MemoryCount = wasm.Memories.Count, + GlobalCount = wasm.Globals.Count, + ElementSegmentCount = wasm.Elements.Count, + DataSegmentCount = wasm.DataSegments.Count, + wasm.DataSize, + TagCount = wasm.Tags.Count, + wasm.StartFunctionIndex, + wasm.DataCount, + wasm.SymbolMapPath, + SymbolMapStatus = wasm.SymbolMapStatus.ToString(), + wasm.SymbolMapEntryCount, + wasm.TargetFeatures, + wasm.ProducerFields, + wasm.Diagnostic + }; + } + + /// + /// Builds a section-table payload for a WebAssembly module. Each section keeps its raw id, + /// display name, file payload offset, and payload size so callers can jump to the bytes that + /// the SDK emitted. + /// + /// The analyzer that opened a raw Wasm module. + /// A JSON-ready section table payload. + public static object BuildSections(AssemblyAnalyzer analyzer) + { + var wasm = RequireWasm(analyzer); + return new + { + analyzer.FilePath, + SectionCount = wasm.Sections.Count, + Sections = wasm.Sections.Select(static s => new + { + s.Id, + s.Name, + s.FileOffset, + s.Size + }).ToArray() + }; + } + + /// + /// Builds a function inventory for a WebAssembly module. Imported functions and file-backed + /// defined functions share the same Wasm function-index space, matching direct call operands + /// and symbol-map entries. + /// + /// The analyzer that opened a raw Wasm module. + /// A JSON-ready function inventory payload. + public static object BuildFunctions(AssemblyAnalyzer analyzer) + { + var wasm = RequireWasm(analyzer); + return new + { + analyzer.FilePath, + FunctionCount = wasm.Functions.Count, + Functions = wasm.Functions.Select(static f => new + { + f.Index, + f.Name, + f.NameSource, + f.IsImported, + f.ImportModule, + f.ImportName, + f.IsExported, + f.ExportNames, + f.TypeIndex, + f.BodyOffset, + f.BodySize, + f.CodeOffset, + f.CodeSize, + ParamTypes = f.ParamTypes.Select(ValueTypeName).ToArray(), + ResultTypes = f.ResultTypes.Select(ValueTypeName).ToArray() + }).ToArray() + }; + } + + private static WasmModuleInfo RequireWasm(AssemblyAnalyzer analyzer) + { + if (analyzer.WasmModuleInfo is { } wasm) + return wasm; + + throw new InvalidOperationException("The analyzer is not a WebAssembly module."); + } + + private static string ValueTypeName(byte valueType) => valueType switch + { + 0x7F => "i32", + 0x7E => "i64", + 0x7D => "f32", + 0x7C => "f64", + 0x7B => "v128", + 0x70 => "funcref", + 0x6F => "externref", + _ => $"0x{valueType:X2}", + }; +} diff --git a/src/Dotsider.Core/Protocol/WebcilPayloadBuilder.cs b/src/Dotsider.Core/Protocol/WebcilPayloadBuilder.cs new file mode 100644 index 00000000..9c7088e8 --- /dev/null +++ b/src/Dotsider.Core/Protocol/WebcilPayloadBuilder.cs @@ -0,0 +1,34 @@ +using Dotsider.Core.Analysis; + +namespace Dotsider.Core.Protocol; + +/// +/// Builds JSON-ready Webcil payloads shared by CLI, MCP, and diagnostics session output. +/// Webcil is a managed assembly container used in browser-wasm publishes, so the payload is +/// provenance beside the normal metadata/IL facts rather than a separate native module view. +/// +public static class WebcilPayloadBuilder +{ + /// + /// Builds a compact Webcil summary for protocol surfaces. Returns null when the analyzer did + /// not open a Webcil assembly, allowing callers to include the property unconditionally. + /// + /// The analyzer whose Webcil provenance should be serialized. + /// A JSON-ready Webcil summary object, or null when the analyzer is not Webcil. + public static object? BuildSummary(AssemblyAnalyzer analyzer) + { + if (analyzer.WebcilInfo is not { } webcil) + return null; + + return new + { + webcil.VersionMajor, + webcil.VersionMinor, + webcil.IsWasmWrapped, + webcil.PayloadOffset, + webcil.SectionCount, + webcil.MetadataSize, + webcil.DebugDirectorySize + }; + } +} diff --git a/src/Dotsider.Core/README.md b/src/Dotsider.Core/README.md index 8f86f993..1c061bf0 100644 --- a/src/Dotsider.Core/README.md +++ b/src/Dotsider.Core/README.md @@ -6,11 +6,11 @@ Core library for .NET assembly analysis. Provides analyzers, models, and the dia | Class | Description | |-------|-------------| -| `AssemblyAnalyzer` | Loads a PE file and exposes types, methods, references, PE headers, CLR header, sections, custom attributes, and resources | +| `AssemblyAnalyzer` | Loads PE files, Webcil-managed `.wasm` assemblies, and raw WebAssembly modules, exposing managed metadata when present plus native/Wasm imports, exports, sections, symbols, and disassembly inputs | | `IlDisassembler` | Disassembles method bodies into IL instruction sequences | | `IlNavigationResolver` | Resolves metadata tokens from IL instructions to `IlNavigationTarget` records (file path, type/method, member kind) for go-to-definition | | `StringExtractor` | Extracts user strings, metadata strings, and raw binary strings from an assembly | -| `SizeAnalyzer` | Builds a hierarchical size tree (namespace → type → method) by IL byte size; for Native AOT binaries the tree comes from the compiler's mstat report when present, else from the binary's merged native symbols, with per-assembly subtrees and data categories | +| `SizeAnalyzer` | Builds a hierarchical size tree (namespace → type → method) by IL byte size; Native AOT uses mstat or native symbols, ReadyToRun uses precompiled native ranges, and raw Wasm uses function bodies, data segments, and section payloads | | `DependencyGraphBuilder` | Generates a dependency graph (nodes and edges) from assembly references; routes through `NetFxBinder` for .NET Framework roots so nodes are keyed on the *bound* identity (post-redirect). Native AOT binaries graph the compiled-in assemblies (mstat × DGML join) plus native import modules | | `MstatReader` | Decodes an ILC size report (`.mstat`) — a valid ECMA-335 assembly whose data lives in IL streams — into per-method, per-type, blob, frozen object, RVA field, and resource sizes with assembly attribution, decoded method signatures (overload identity), frozen-object owner attribution, and dependency-graph node names; `Probe` cheaply sniffs whether a file is an mstat at all | | `MstatSizeIndex` | The shared normalization layer over an mstat: rows aggregated under build-stable identity keys, one double-count policy for the 2.1+ detail sections (`MstatSectionPolicy`), owner-based frozen attribution with an explicit `(unattributed)` bucket, and per-assembly / per-namespace byte totals — `SizeAnalyzer`, `MstatDiffer`, and budget evaluation all draw from it so totals never disagree | @@ -20,13 +20,15 @@ Core library for .NET assembly analysis. Provides analyzers, models, and the dia | `DgmlReader` | Streams an ILC dependency-graph DGML file into a `DgmlGraph` whose `PathToRoot` answers "why is this in my binary" — node labels equal mstat node names, joining the two files | | `NativeSymbolReader` | Reads a native binary's symbols — names, addresses, sizes, kinds, and source locations — from its native PDB (MSF/DBI/CodeView with C13 lines and publics), DWARF `.dbg` sidecar (DIE walk, range lists, line programs, `.symtab` data pass), or dSYM bundle (DWARF + nlist merged), validating identity first (GUID+age, build id / debuglink CRC, or UUID) and demangling ILC names against the binary's own recovered metadata; falls back to unwind-data boundaries (`.pdata`, `.eh_frame`, `LC_FUNCTION_STARTS`) when no symbol file exists | | `NativeDisassembler` | Disassembles a native code window into `NativeInstruction`s and a rendered listing for every concrete .NET native architecture dotsider recognizes: x64, Arm64, x86, Arm32/Thumb-2, RISC-V64, LoongArch64, and Wasm32. `DisassembleSymbol` slices one recovered `NativeSymbol`, resolves call/branch/data targets to names (`Foo+0x12`, `loc_…`, `MODULE!Function` via the `NativeImportResolver`), and stamps per-instruction source from the `NativeSourceMap` | +| `WasmModuleReader` | Parses raw WebAssembly modules such as `dotnet.native.wasm`: standard/custom sections, type/table/memory/global declarations, imports, exports, element and data segments, function bodies, name/producers/target-features custom sections, and `dotnet.native.js.symbols` sidecars | +| `WebcilImageReader` | Unwraps Webcil app assemblies from browser-wasm publishes, including Wasm-wrapped payloads, so metadata, IL method bodies, debug directory entries, embedded PDBs, sidecar PDBs, and Source Link flow through the normal managed analyzer path | | `AssemblyDiffer` | Compares two assemblies and reports added, removed, and changed types, methods, and references. Method body comparison uses normalized IL instruction walks with semantic token resolution, deep local signature decoding, and exception region analysis | | `RuntimeTracer` | Launches a .NET process with EventPipe tracing for JIT, GC, exception, and counter events | | `NuGetPackageAnalyzer` | Reads `.nupkg` files for package metadata and DLL listing | | `NuGetDepsJsonResolver` | Resolves assembly references by consulting the referencing assembly's `.deps.json` to locate NuGet dependencies in the global packages folder | | `SingleFileBundleReader` | Detects and reads .NET single-file bundles — parses the manifest and extracts individual entries | | `DotNetRuntimeLocator` | Discovers system .NET installations and resolves shared framework assembly paths | -| `AssemblyLoader` | Opens assemblies with automatic apphost companion redirect and single-file bundle entry extraction | +| `AssemblyLoader` | Opens assemblies, apphosts, single-file bundles, Native AOT binaries, Webcil assemblies, and raw WebAssembly modules with the right analyzer path for each | | `ApphostDetector` | Detects .NET apphost executables, locates companion managed assemblies, and identifies single-file bundles | | `ImplementationAssemblyResolver` | Maps reference assemblies to their implementations via known mappings, type forwarders, bundles, and shared framework probing | | `NetFxBinder` | CLR-accurate .NET Framework binder. Walks the real fusion order — framework unification + machine.config + publisher policy + app config, then GAC + framework runtime directory + `` + app base + `` — so the dep graph reflects what the actual CLR loads. Switches probe locations and GAC token format on `NetFxRuntimeVersion`: CLR 4 (`%WINDIR%\Microsoft.NET\assembly`, `v4.0_…` tokens, `v4.0.30319` runtime) and CLR 2 (`%WINDIR%\assembly`, no-prefix tokens, `v2.0.50727` runtime) | diff --git a/src/Dotsider.Mcp/README.md b/src/Dotsider.Mcp/README.md index 6a3b919a..1e30edda 100644 --- a/src/Dotsider.Mcp/README.md +++ b/src/Dotsider.Mcp/README.md @@ -2,7 +2,7 @@ MCP server for .NET assembly analysis. Works in two modes: -- **Direct mode** — pass an `assemblyPath` to analyze any `.dll` or `.exe` on disk +- **Direct mode** — pass an `assemblyPath` to analyze any `.dll`, `.exe`, Webcil app `.wasm`, or supported native module such as `dotnet.native.wasm` on disk - **Session mode** — pass a `sessionId` (PID) to query a running dotsider TUI instance over its Unix domain socket Most tools accept either parameter. Session-only tools (navigation, tracing) require a running instance. @@ -25,7 +25,7 @@ Most tools accept either parameter. Session-only tools (navigation, tracing) req | Tool | Description | |------|-------------| -| `get_assembly_info` | Name, version, framework, architecture, binary kind, and member counts | +| `get_assembly_info` | Name, version, framework, architecture, binary kind, member counts, and native-format summaries | | `list_types` | Type definitions with optional name filter and limit | | `list_methods` | Method definitions with optional type/name filter | | `find_members` | Search types, methods, and member refs by query string | @@ -54,7 +54,7 @@ Most tools accept either parameter. Session-only tools (navigation, tracing) req | Tool | Description | |------|-------------| -| `get_size_breakdown` | Hierarchical size tree: namespace > type > method | +| `get_size_breakdown` | Hierarchical size tree: IL, Native AOT, ReadyToRun, or Wasm payload size depending on input | | `get_largest_methods` | Top methods by IL byte size, or Native AOT native size when mstat is available | ### Native AOT @@ -70,8 +70,15 @@ Most tools accept either parameter. Session-only tools (navigation, tracing) req | Tool | Description | |------|-------------| -| `get_native_symbols` | Native symbols from Native AOT, ReadyToRun, and native binaries | -| `get_native_disassembly` | Native disassembly for x64, Arm64, x86, Arm32/Thumb-2, RISC-V64, LoongArch64, and Wasm32 | +| `get_native_symbols` | Native symbols from Native AOT, ReadyToRun, raw Wasm modules, and native binaries | +| `get_native_disassembly` | Native disassembly for x64, Arm64, x86, Arm32/Thumb-2, RISC-V64, LoongArch64, and Wasm32; raw Wasm accepts names, `func:N`, decimal function indexes, and `0x…` offsets | + +### WebAssembly + +| Tool | Description | +|------|-------------| +| `list_wasm_sections` | Raw Wasm section table with payload offsets and sizes | +| `list_wasm_functions` | Imported and defined Wasm functions in function-index order | ### Correlation diff --git a/src/Dotsider.Mcp/Tools/AssemblyTools.cs b/src/Dotsider.Mcp/Tools/AssemblyTools.cs index 3b695080..6ecf9970 100644 --- a/src/Dotsider.Mcp/Tools/AssemblyTools.cs +++ b/src/Dotsider.Mcp/Tools/AssemblyTools.cs @@ -13,7 +13,7 @@ public sealed partial class AssemblyTools(DotsiderSessionManager sessionManager) /// /// Gets assembly metadata including name, version, framework, architecture, and member counts. /// - /// Path to assembly file (.dll or .exe). + /// Path to an assembly file or supported native module. /// PID of a running dotsider instance. /// Cancellation token. /// JSON with assembly identity and statistics. @@ -53,7 +53,9 @@ public async partial Task GetAssemblyInfo( NativeSymbolSource = analyzer.NativeSymbols?.Source, NativeSymbolStatus = analyzer.NativeSymbols?.Status, PreIlc = BuildPreIlcSummary(analyzer), - ReadyToRun = BuildReadyToRunSummary(analyzer) + ReadyToRun = BuildReadyToRunSummary(analyzer), + Webcil = WebcilPayloadBuilder.BuildSummary(analyzer), + Wasm = WasmPayloadBuilder.BuildSummary(analyzer) }, DotsiderJsonOptions.Default); } diff --git a/src/Dotsider.Mcp/Tools/SymbolTools.cs b/src/Dotsider.Mcp/Tools/SymbolTools.cs index fbf5b70f..ae7f2050 100644 --- a/src/Dotsider.Mcp/Tools/SymbolTools.cs +++ b/src/Dotsider.Mcp/Tools/SymbolTools.cs @@ -6,14 +6,15 @@ namespace Dotsider.Mcp.Tools; /// -/// MCP tools for native symbols: function names, addresses, and sizes read from a Native AOT -/// binary's PDB, DWARF, or dSYM — or unwind-data boundaries when no symbol file exists. +/// MCP tools for native symbols: function names, addresses, and sizes recovered from Native AOT, +/// ReadyToRun, raw WebAssembly, and platform-native binaries. Provenance identifies whether names +/// came from symbol files, runtime maps, Wasm sidecars, or fallback boundaries. /// [McpServerToolType] public sealed partial class SymbolTools(DotsiderSessionManager sessionManager) { /// - /// Gets a binary's native symbols with their provenance (source, status, symbol file path). + /// Gets a binary's native symbols with their provenance (source, status, and optional sidecar path). /// /// Path to the binary file. /// PID of a running dotsider instance. diff --git a/src/Dotsider.Mcp/Tools/ToolHelpers.cs b/src/Dotsider.Mcp/Tools/ToolHelpers.cs index 2d38b01d..8e61e9b2 100644 --- a/src/Dotsider.Mcp/Tools/ToolHelpers.cs +++ b/src/Dotsider.Mcp/Tools/ToolHelpers.cs @@ -15,9 +15,9 @@ namespace Dotsider.Mcp.Tools; internal static class ToolHelpers { /// - /// Validates that an assembly path is provided and points to an existing file. + /// Validates that an analysis input path is provided and points to an existing file. /// - /// The assembly file path to validate. + /// The assembly or native module path to validate. /// /// Thrown when is null/empty or the file does not exist. /// @@ -46,11 +46,11 @@ internal static void ValidateFilePath(string? path, string label) } /// - /// Opens an assembly file, handling apphosts and single-file bundles. + /// Opens an analysis input, handling apphosts, single-file bundles, and raw native modules. /// For apphosts, silently redirects to the companion managed .dll. /// For single-file bundles, extracts and returns the entry assembly. /// - /// Path to the assembly file. + /// Path to the assembly or native module file. /// The opened analyzer. /// Thrown if the file cannot be opened. internal static AssemblyAnalyzer OpenAnalyzer(string path) diff --git a/src/Dotsider.Mcp/Tools/WasmTools.cs b/src/Dotsider.Mcp/Tools/WasmTools.cs new file mode 100644 index 00000000..1d4952f1 --- /dev/null +++ b/src/Dotsider.Mcp/Tools/WasmTools.cs @@ -0,0 +1,85 @@ +using Dotsider.Core.Protocol; +using ModelContextProtocol; +using ModelContextProtocol.Server; +using System.Text.Json; + +namespace Dotsider.Mcp.Tools; + +/// +/// MCP tools for raw .NET WebAssembly runtime modules such as dotnet.native.wasm. +/// Webcil app assemblies are managed metadata and should use the normal assembly and IL tools. +/// These tools expose Wasm sections and function-index inventories for agents and automation. +/// +[McpServerToolType] +public sealed partial class WasmTools(DotsiderSessionManager sessionManager) +{ + /// + /// Lists the sections of a raw WebAssembly module, preserving payload offsets and sizes. + /// + /// Path to a raw WebAssembly module. + /// PID of a running dotsider instance. + /// Cancellation token. + /// JSON section table for the raw Wasm module. + [McpServerTool(ReadOnly = true, OpenWorld = false)] + public async partial Task ListWasmSections( + string? assemblyPath = null, + int? sessionId = null, + CancellationToken ct = default) + { + if (assemblyPath is not null) + { + ToolHelpers.ValidateAssemblyPath(assemblyPath); + using var analyzer = ToolHelpers.OpenAnalyzer(assemblyPath); + return Serialize(() => WasmPayloadBuilder.BuildSections(analyzer)); + } + + if (sessionId is not null) + { + return await sessionManager.GetTarget(sessionId.Value) + .SendAndUnwrapAsync(new DotsiderRequest { Method = "list-wasm-sections" }, ct); + } + + return "Error: Either assemblyPath or sessionId is required."; + } + + /// + /// Lists imported and defined functions in WebAssembly function-index order. + /// + /// Path to a raw WebAssembly module. + /// PID of a running dotsider instance. + /// Cancellation token. + /// JSON function inventory for the raw Wasm module. + [McpServerTool(ReadOnly = true, OpenWorld = false)] + public async partial Task ListWasmFunctions( + string? assemblyPath = null, + int? sessionId = null, + CancellationToken ct = default) + { + if (assemblyPath is not null) + { + ToolHelpers.ValidateAssemblyPath(assemblyPath); + using var analyzer = ToolHelpers.OpenAnalyzer(assemblyPath); + return Serialize(() => WasmPayloadBuilder.BuildFunctions(analyzer)); + } + + if (sessionId is not null) + { + return await sessionManager.GetTarget(sessionId.Value) + .SendAndUnwrapAsync(new DotsiderRequest { Method = "list-wasm-functions" }, ct); + } + + return "Error: Either assemblyPath or sessionId is required."; + } + + private static string Serialize(Func build) + { + try + { + return JsonSerializer.Serialize(build(), DotsiderJsonOptions.Default); + } + catch (InvalidOperationException ex) + { + throw new McpException(ex.Message); + } + } +} diff --git a/src/Dotsider/Commands/AnalyzeCommand.cs b/src/Dotsider/Commands/AnalyzeCommand.cs index 16306773..6950da0b 100644 --- a/src/Dotsider/Commands/AnalyzeCommand.cs +++ b/src/Dotsider/Commands/AnalyzeCommand.cs @@ -1,19 +1,21 @@ using Dotsider.Core.Analysis; using Dotsider.Core.Analysis.Disasm; using Dotsider.Core.Analysis.Models; +using Dotsider.Core.Protocol; using Dotsider.Infrastructure; using System.CommandLine; namespace Dotsider.Commands; /// -/// Headless assembly analysis command: types, methods, IL, deps, strings, size, symbols. +/// Headless file analysis command: managed metadata, IL, deps, strings, size, native symbols, +/// ReadyToRun correlation, Native AOT sidecars, and raw WebAssembly modules. /// internal static class AnalyzeCommand { private static readonly Argument s_fileArg = new("file") { - Description = "Assembly file to analyze (.dll or .exe)" + Description = "Assembly or native module to analyze (.dll, .exe, or .wasm)" }; private static readonly Option s_typesOption = new("--types") @@ -296,6 +298,8 @@ private static int PrintAssemblyInfo(AssemblyAnalyzer a, OutputFormatter fmt) a.NativeSymbolsPath, PreIlc = BuildPreIlcProbeJson(a), ReadyToRun = BuildReadyToRunJson(a), + Webcil = WebcilPayloadBuilder.BuildSummary(a), + Wasm = WasmPayloadBuilder.BuildSummary(a), Types = a.TypeDefs, Methods = a.MethodDefs, References = a.AssemblyRefs }); } @@ -341,6 +345,30 @@ private static int PrintAssemblyInfo(AssemblyAnalyzer a, OutputFormatter fmt) if (r2r.Diagnostic is { } diagnostic) fmt.WriteLine($"Note: {diagnostic}"); } + else if (a.WasmModuleInfo is { } wasm) + { + fmt.WriteLine("Kind: WebAssembly (.NET)"); + fmt.WriteLine($"Wasm: v{wasm.Version}, {wasm.Sections.Count} sections"); + fmt.WriteLine($"Functions: {wasm.DefinedFunctionCount} defined, {wasm.ImportedFunctionCount} imported"); + fmt.WriteLine($"Code: {DotsiderState.FormatSize(wasm.CodeSize)}"); + fmt.WriteLine($"Data: {wasm.DataSegments.Count} segments, {DotsiderState.FormatSize(wasm.DataSize)}"); + fmt.WriteLine($"Imports: {wasm.Imports.Count} entries"); + fmt.WriteLine($"Exports: {wasm.Exports.Count} entries"); + fmt.WriteLine($"Symbols: {wasm.SymbolMapStatus}" + + (wasm.SymbolMapEntryCount > 0 ? $", {wasm.SymbolMapEntryCount} names" : "")); + if (wasm.SymbolMapPath is { } symbolMapPath) + fmt.WriteLine($"Symbol Map: {symbolMapPath}"); + if (wasm.Diagnostic is { } diagnostic) + fmt.WriteLine($"Note: {diagnostic}"); + } + else if (a.WebcilInfo is { } webcil) + { + fmt.WriteLine("Kind: Managed"); + fmt.WriteLine($"Webcil: v{webcil.VersionMajor}.{webcil.VersionMinor}" + + (webcil.IsWasmWrapped ? " (Wasm-wrapped)" : "")); + fmt.WriteLine($"Webcil: {webcil.SectionCount} sections, " + + $"{DotsiderState.FormatSize(webcil.MetadataSize)} metadata"); + } fmt.WriteLine($"PDB: {a.PdbProvenance}"); fmt.WriteLine($"SourceLink: {(a.SourceLink.IsPresent ? $"present, {a.SourceLink.Mappings.Count} mappings" : "not present")}"); if (a.IsBundleBacked) diff --git a/src/Dotsider/Diagnostics/DotsiderDiagnosticsListener.cs b/src/Dotsider/Diagnostics/DotsiderDiagnosticsListener.cs index 10efa885..0fae3cac 100644 --- a/src/Dotsider/Diagnostics/DotsiderDiagnosticsListener.cs +++ b/src/Dotsider/Diagnostics/DotsiderDiagnosticsListener.cs @@ -252,6 +252,10 @@ private DotsiderResponse HandleRequest(DotsiderRequest request) "get-native-symbols" => HandleGetNativeSymbols(), "disassemble-native" => HandleDisassembleNative(request), + // WebAssembly + "list-wasm-sections" => HandleListWasmSections(), + "list-wasm-functions" => HandleListWasmFunctions(), + // Pre-ILC correlation "correlate-method" => HandleCorrelateMethod(request), @@ -352,7 +356,9 @@ private DotsiderResponse HandleAssemblyInfo() FrozenStringCount = a.FrozenStrings.Count, NativeSymbolCount = a.NativeSymbols?.Symbols.Count ?? 0, NativeSymbolSource = a.NativeSymbols?.Source, - NativeSymbolStatus = a.NativeSymbols?.Status + NativeSymbolStatus = a.NativeSymbols?.Status, + Webcil = WebcilPayloadBuilder.BuildSummary(a), + Wasm = WasmPayloadBuilder.BuildSummary(a) }); } @@ -709,6 +715,30 @@ private DotsiderResponse HandleDisassembleNative(DotsiderRequest request) : DotsiderResponse.Ok(new { Symbol = matches[0].ManagedName ?? matches[0].Name, a.Architecture, result.Value.Instructions }); } + private DotsiderResponse HandleListWasmSections() + { + try + { + return DotsiderResponse.Ok(WasmPayloadBuilder.BuildSections(RequireAnalyzer())); + } + catch (InvalidOperationException ex) + { + return DotsiderResponse.Fail(ex.Message); + } + } + + private DotsiderResponse HandleListWasmFunctions() + { + try + { + return DotsiderResponse.Ok(WasmPayloadBuilder.BuildFunctions(RequireAnalyzer())); + } + catch (InvalidOperationException ex) + { + return DotsiderResponse.Fail(ex.Message); + } + } + private static DotsiderResponse HandleDisassembleReadyToRun(Core.Analysis.AssemblyAnalyzer a, string target) { var result = Core.Analysis.ReadyToRunCorrelationQuery.Resolve(a, target, CancellationToken.None); diff --git a/src/Dotsider/Views/GeneralView.cs b/src/Dotsider/Views/GeneralView.cs index e3f88d6f..b9f9b542 100644 --- a/src/Dotsider/Views/GeneralView.cs +++ b/src/Dotsider/Views/GeneralView.cs @@ -154,6 +154,42 @@ public static Hex1bWidget Build(WidgetContext ctx, DotsiderState s if (r2r.Diagnostic is { } diagnostic) infoLines.Add($" Note: {diagnostic}"); } + else if (analyzer.WasmModuleInfo is { } wasm) + { + infoLines.Add(""); + infoLines.Add(" Binary Kind: WebAssembly (.NET)"); + infoLines.Add($" Wasm Version: {wasm.Version}"); + infoLines.Add($" Wasm Sections: {wasm.Sections.Count}"); + infoLines.Add($" Types: {wasm.Types.Count}"); + infoLines.Add($" Functions: {wasm.DefinedFunctionCount} defined, {wasm.ImportedFunctionCount} imported"); + infoLines.Add($" Code Size: {DotsiderState.FormatSize(wasm.CodeSize)}"); + infoLines.Add($" Tables/Memories: {wasm.Tables.Count} / {wasm.Memories.Count}"); + infoLines.Add($" Globals/Elements: {wasm.Globals.Count} / {wasm.Elements.Count}"); + infoLines.Add($" Data Segments: {wasm.DataSegments.Count}, {DotsiderState.FormatSize(wasm.DataSize)}"); + if (wasm.StartFunctionIndex is { } start) + infoLines.Add($" Start Function: func:{start}"); + infoLines.Add($" Imports: {wasm.Imports.Count}"); + infoLines.Add($" Exports: {wasm.Exports.Count}"); + infoLines.Add($" Symbol Map: {wasm.SymbolMapStatus}" + + (wasm.SymbolMapEntryCount > 0 ? $" ({wasm.SymbolMapEntryCount} names)" : "")); + if (wasm.SymbolMapPath is { } symbolMapPath) + infoLines.Add($" Symbol Map Path: {symbolMapPath}"); + if (analyzer.NativeSymbols is { } symbols) + infoLines.Add(symbols.Symbols.Count > 0 + ? $" Native Symbols: {symbols.Symbols.Count} from {symbols.Source}" + : $" Native Symbols: {symbols.Diagnostic ?? symbols.Status.ToString()}"); + if (wasm.Diagnostic is { } diagnostic) + infoLines.Add($" Note: {diagnostic}"); + } + else if (analyzer.WebcilInfo is { } webcil) + { + infoLines.Add(""); + infoLines.Add(" Binary Kind: Managed Webcil (.NET)"); + infoLines.Add($" Webcil Format: v{webcil.VersionMajor}.{webcil.VersionMinor}"); + infoLines.Add($" Wasm Wrapped: {(webcil.IsWasmWrapped ? "Yes" : "No")}"); + infoLines.Add($" Webcil Sections: {webcil.SectionCount}"); + infoLines.Add($" Webcil Metadata: {DotsiderState.FormatSize(webcil.MetadataSize)}"); + } var infoText = string.Join("\n", infoLines); @@ -161,7 +197,10 @@ public static Hex1bWidget Build(WidgetContext ctx, DotsiderState s // horizontal scrollbar (long publish-dir PDB paths overflow the width) // doesn't cover the last info line. var infoHeight = infoLines.Count - + (analyzer.NativeAotInfo is null && analyzer.ReadyToRunInfo is null ? 2 : 3); + + (analyzer.NativeAotInfo is null + && analyzer.ReadyToRunInfo is null + && analyzer.WasmModuleInfo is null + && analyzer.WebcilInfo is null ? 2 : 3); if (state.GeneralInfoEditorText != infoText) { @@ -180,9 +219,7 @@ public static Hex1bWidget Build(WidgetContext ctx, DotsiderState s return ctx.VStack(outer => { - var widgets = new List - { - // Assembly Info section (read-only editor for text selection + yank) + var infoPanel = outer.Border( outer.ThemePanel(t => t .Set(EditorTheme.SelectionForegroundColor, Hex1bColor.Default) @@ -205,7 +242,13 @@ public static Hex1bWidget Build(WidgetContext ctx, DotsiderState s () => state.App.Invalidate()); }) .FillWidth().FillHeight()) - ).Title(" Assembly Info ").FixedHeight(infoHeight) + ).Title(" Assembly Info "); + + var widgets = new List + { + // Rich binary summaries can outgrow the viewport; split those with + // the references table so neither section disappears after resize. + infoHeight > 20 ? infoPanel.FillHeight(2) : infoPanel.FixedHeight(infoHeight) }; // Search bar diff --git a/src/Dotsider/Views/IlInspectorView.cs b/src/Dotsider/Views/IlInspectorView.cs index 82dafb1b..852ec9b7 100644 --- a/src/Dotsider/Views/IlInspectorView.cs +++ b/src/Dotsider/Views/IlInspectorView.cs @@ -466,6 +466,9 @@ private static void AppendManagedRows( /// The current application state. internal static List BuildNativeTreeRows(DotsiderState state) { + if (state.Analyzer.WasmModuleInfo is { } wasm) + return BuildWasmTreeRows(state, wasm); + var rows = new List(); var info = state.Analyzer.NativeSymbols; if (info is null) return rows; @@ -535,6 +538,113 @@ internal static List BuildNativeTreeRows(DotsiderState state) return rows; } + private static List BuildWasmTreeRows(DotsiderState state, WasmModuleInfo wasm) + { + var rows = new List(); + var query = state.Search[TabId.IlInspector].Query ?? string.Empty; + IReadOnlyDictionary symbolByOffset = state.Analyzer.NativeSymbols?.Symbols + .Where(static s => s.FileOffset is not null) + .ToDictionary(static s => s.FileOffset!.Value) + ?? []; + + AddWasmImportRows(state, rows, wasm, query); + AddWasmFunctionGroup(state, rows, wasm, symbolByOffset, query, + "(exports)", + static f => !f.IsImported && f.IsExported, + static f => $"func[{f.Index}] {f.ExportNames[0]} -> {f.Name}"); + AddWasmFunctionGroup(state, rows, wasm, symbolByOffset, query, + "(functions)", + static f => !f.IsImported && !f.IsExported && f.NameSource != "synthetic", + static f => $"func[{f.Index}] {f.Name}"); + AddWasmFunctionGroup(state, rows, wasm, symbolByOffset, query, + "(synthetic)", + static f => !f.IsImported && !f.IsExported && f.NameSource == "synthetic", + static f => $"func[{f.Index}] {f.Name}"); + + return rows; + } + + private static void AddWasmImportRows( + DotsiderState state, + List rows, + WasmModuleInfo wasm, + string query) + { + var imports = wasm.Functions + .Where(static f => f.IsImported) + .Where(f => WasmFunctionMatches(f, query)) + .OrderBy(static f => f.Index) + .ToList(); + if (imports.Count == 0) return; + + var groupKey = "wasm:imports"; + var expanded = GetExpansionState(state, groupKey, defaultExpanded: query.Length > 0); + rows.Add(new IlTreeRow(groupKey, 0, IlTreeRowKind.Namespace, + "(imports)", null, CanExpand: true, IsExpanded: expanded, groupKey)); + if (!expanded) return; + + foreach (var byModule in imports.GroupBy(static f => f.ImportModule ?? "(module)", StringComparer.Ordinal)) + { + var moduleKey = $"wasm:import-module:{byModule.Key}"; + var moduleExpanded = GetExpansionState(state, moduleKey, defaultExpanded: query.Length > 0); + rows.Add(new IlTreeRow(moduleKey, 1, IlTreeRowKind.Type, + HighlightHelper.HighlightSubstring(byModule.Key, query), null, + CanExpand: true, IsExpanded: moduleExpanded, moduleKey)); + if (!moduleExpanded) continue; + + foreach (var function in byModule) + { + var label = $"func[{function.Index}] {function.ImportName ?? function.Name}"; + rows.Add(new IlTreeRow($"wasm:import:{function.Index}", 2, IlTreeRowKind.Method, + HighlightHelper.HighlightSubstring(label, query), null, + CanExpand: false, IsExpanded: false, "")); + } + } + } + + private static void AddWasmFunctionGroup( + DotsiderState state, + List rows, + WasmModuleInfo wasm, + IReadOnlyDictionary symbolByOffset, + string query, + string groupName, + Func predicate, + Func label) + { + var functions = wasm.Functions + .Where(predicate) + .Where(f => f.CodeOffset is not null && symbolByOffset.ContainsKey(f.CodeOffset.Value)) + .Where(f => WasmFunctionMatches(f, query)) + .OrderBy(static f => f.Index) + .ToList(); + if (functions.Count == 0) return; + + var groupKey = $"wasm:{groupName}"; + var expanded = GetExpansionState(state, groupKey, defaultExpanded: query.Length > 0); + rows.Add(new IlTreeRow(groupKey, 0, IlTreeRowKind.Namespace, + groupName, null, CanExpand: true, IsExpanded: expanded, groupKey)); + if (!expanded) return; + + foreach (var function in functions) + { + var symbol = symbolByOffset[function.CodeOffset!.Value]; + var rowLabel = label(function); + rows.Add(new IlTreeRow($"wasm:func:{function.Index}", 1, IlTreeRowKind.Method, + HighlightHelper.HighlightSubstring(rowLabel, query), null, + CanExpand: false, IsExpanded: false, "", symbol)); + } + } + + private static bool WasmFunctionMatches(WasmFunctionInfo function, string query) + { + if (query.Length == 0) return true; + return function.Name.Contains(query, StringComparison.OrdinalIgnoreCase) + || function.ImportModule?.Contains(query, StringComparison.OrdinalIgnoreCase) == true + || function.ImportName?.Contains(query, StringComparison.OrdinalIgnoreCase) == true + || function.ExportNames.Any(e => e.Contains(query, StringComparison.OrdinalIgnoreCase)); + } + /// /// Formats a tree row for display in the table cell, adding indentation, /// expand/collapse glyphs, and the selection marker. diff --git a/src/Dotsider/Views/PeMetadataView.cs b/src/Dotsider/Views/PeMetadataView.cs index 357b38ec..a1c9e572 100644 --- a/src/Dotsider/Views/PeMetadataView.cs +++ b/src/Dotsider/Views/PeMetadataView.cs @@ -63,6 +63,23 @@ public static Hex1bWidget Build(WidgetContext ctx, DotsiderState s // Ensure the first row is focused when arriving at a sub-tab state.PeFocusedKey ??= state.PeSubTab switch { + PeSubTabId.Sections when analyzer.WasmModuleInfo is { Sections.Count: > 0 } wasm => + GetWasmSectionKey(wasm.Sections[0]), + PeSubTabId.TypeDef when analyzer.WasmModuleInfo is { Types.Count: > 0 } wasm => + GetWasmTypeKey(wasm.Types[0]), + PeSubTabId.MethodDef when analyzer.WasmModuleInfo is { Functions.Count: > 0 } wasm => + GetWasmFunctionKey(wasm.Functions[0]), + PeSubTabId.TypeRef when analyzer.WasmModuleInfo is { Tables.Count: > 0 } wasm => + GetWasmTableKey(wasm.Tables[0]), + PeSubTabId.MemberRef when analyzer.WasmModuleInfo is { Memories.Count: > 0 } wasm => + GetWasmMemoryKey(wasm.Memories[0]), + PeSubTabId.Attributes when analyzer.WasmModuleInfo is { Globals.Count: > 0 } wasm => + GetWasmGlobalKey(wasm.Globals[0]), + PeSubTabId.Resources when analyzer.WasmModuleInfo is { DataSegments.Count: > 0 } wasm => + GetWasmDataSegmentKey(wasm.DataSegments[0]), + PeSubTabId.DebugDirectory when analyzer.WasmModuleInfo is { Sections.Count: > 0 } wasm + && wasm.Sections.FirstOrDefault(static s => s.Id == 0) is { } custom => + GetWasmSectionKey(custom), PeSubTabId.Sections when analyzer.Sections.Count > 0 => analyzer.Sections[0].Name, PeSubTabId.TypeDef when metadataAnalyzer.TypeDefs.Count > 0 => metadataAnalyzer.TypeDefs[0].Token, PeSubTabId.MethodDef when metadataAnalyzer.MethodDefs.Count > 0 => metadataAnalyzer.MethodDefs[0].Token, @@ -73,14 +90,24 @@ public static Hex1bWidget Build(WidgetContext ctx, DotsiderState s PeSubTabId.Resources when metadataAnalyzer.Resources.Count > 0 => metadataAnalyzer.Resources[0].Name, PeSubTabId.DebugDirectory when GetDebugDirectoryRows(state).Count > 0 => GetDebugDirectoryRowKey(GetDebugDirectoryRows(state)[0]), + PeSubTabId.Imports when analyzer.WasmModuleInfo is { Imports.Count: > 0 } wasm => + GetWasmImportKey(wasm.Imports[0]), PeSubTabId.Imports when GetImportRows(analyzer).Count > 0 => GetImportRows(analyzer)[0].Key, + PeSubTabId.Exports when analyzer.WasmModuleInfo is { Exports.Count: > 0 } wasm => + GetWasmExportKey(wasm.Exports[0]), PeSubTabId.Exports when analyzer.Exports.Count > 0 => analyzer.Exports[0].Ordinal, + PeSubTabId.LoadConfig when analyzer.WasmModuleInfo is { Elements.Count: > 0 } wasm => + GetWasmElementKey(wasm.Elements[0]), PeSubTabId.LoadConfig when analyzer.LoadConfig is not null => GetLoadConfigRows(analyzer.LoadConfig)[0].Field, + PeSubTabId.RtrSections when analyzer.WasmModuleInfo is { Tags.Count: > 0 } wasm => + GetWasmTagKey(wasm.Tags[0]), PeSubTabId.RtrSections when analyzer.ReadyToRunSections.Count > 0 => analyzer.ReadyToRunSections[0].SectionId, + PeSubTabId.AotTypes when analyzer.WasmModuleInfo is not null => + GetWasmModuleInfoKey("version"), PeSubTabId.AotTypes when analyzer.RecoveredTypes.Count > 0 => analyzer.RecoveredTypes[0].FullName, PeSubTabId.Symbols when GetSymbolRows(analyzer).Count > 0 => @@ -227,33 +254,33 @@ PeSubTabId.Symbols when GetSymbolRows(analyzer).Count > 0 => // Bottom section: Metadata tables in sub-tabs Hex1bWidget metadataTabs = outer.TabPanel(tp => [ - tp.Tab("Sections", t => [BuildSectionsTable(t, state)]) + tp.Tab(GetPeSubTabLabel(state, PeSubTabId.Sections), t => [BuildSectionsTable(t, state)]) .Selected(state.PeSubTab == PeSubTabId.Sections), - tp.Tab("TypeDef", t => [BuildTypeDefsTable(t, state)]) + tp.Tab(GetPeSubTabLabel(state, PeSubTabId.TypeDef), t => [BuildTypeDefsTable(t, state)]) .Selected(state.PeSubTab == PeSubTabId.TypeDef), - tp.Tab("MethodDef", t => [BuildMethodDefsTable(t, state)]) + tp.Tab(GetPeSubTabLabel(state, PeSubTabId.MethodDef), t => [BuildMethodDefsTable(t, state)]) .Selected(state.PeSubTab == PeSubTabId.MethodDef), - tp.Tab("TypeRef", t => [BuildTypeRefsTable(t, state)]) + tp.Tab(GetPeSubTabLabel(state, PeSubTabId.TypeRef), t => [BuildTypeRefsTable(t, state)]) .Selected(state.PeSubTab == PeSubTabId.TypeRef), - tp.Tab("MemberRef", t => [BuildMemberRefsTable(t, state)]) + tp.Tab(GetPeSubTabLabel(state, PeSubTabId.MemberRef), t => [BuildMemberRefsTable(t, state)]) .Selected(state.PeSubTab == PeSubTabId.MemberRef), - tp.Tab("Attributes", t => [BuildAttributesTable(t, state)]) + tp.Tab(GetPeSubTabLabel(state, PeSubTabId.Attributes), t => [BuildAttributesTable(t, state)]) .Selected(state.PeSubTab == PeSubTabId.Attributes), - tp.Tab("Resources", t => [BuildResourcesTable(t, state)]) + tp.Tab(GetPeSubTabLabel(state, PeSubTabId.Resources), t => [BuildResourcesTable(t, state)]) .Selected(state.PeSubTab == PeSubTabId.Resources), - tp.Tab("Debug Directory", t => [BuildDebugDirectoryTable(t, state)]) + tp.Tab(GetPeSubTabLabel(state, PeSubTabId.DebugDirectory), t => [BuildDebugDirectoryTable(t, state)]) .Selected(state.PeSubTab == PeSubTabId.DebugDirectory), - tp.Tab("Imports", t => [BuildImportsTable(t, state)]) + tp.Tab(GetPeSubTabLabel(state, PeSubTabId.Imports), t => [BuildImportsTable(t, state)]) .Selected(state.PeSubTab == PeSubTabId.Imports), - tp.Tab("Exports", t => [BuildExportsTable(t, state)]) + tp.Tab(GetPeSubTabLabel(state, PeSubTabId.Exports), t => [BuildExportsTable(t, state)]) .Selected(state.PeSubTab == PeSubTabId.Exports), - tp.Tab("Load Config", t => [BuildLoadConfigTable(t, state)]) + tp.Tab(GetPeSubTabLabel(state, PeSubTabId.LoadConfig), t => [BuildLoadConfigTable(t, state)]) .Selected(state.PeSubTab == PeSubTabId.LoadConfig), - tp.Tab("R2R Sections", t => [BuildRtrSectionsTable(t, state)]) + tp.Tab(GetPeSubTabLabel(state, PeSubTabId.RtrSections), t => [BuildRtrSectionsTable(t, state)]) .Selected(state.PeSubTab == PeSubTabId.RtrSections), - tp.Tab("AOT Types", t => [BuildAotTypesTable(t, state)]) + tp.Tab(GetPeSubTabLabel(state, PeSubTabId.AotTypes), t => [BuildAotTypesTable(t, state)]) .Selected(state.PeSubTab == PeSubTabId.AotTypes), - tp.Tab("Symbols", t => [BuildSymbolsTable(t, state)]) + tp.Tab(GetPeSubTabLabel(state, PeSubTabId.Symbols), t => [BuildSymbolsTable(t, state)]) .Selected(state.PeSubTab == PeSubTabId.Symbols) ]) .OnSelectionChanged(e => @@ -431,7 +458,93 @@ state.PeDetailContent is not null && state.PeDetailEditorState is not null ]).Fill(); } - private static TableWidget BuildSectionsTable(WidgetContext ctx, DotsiderState state) + private static Hex1bWidget BuildSectionsTable(WidgetContext ctx, DotsiderState state) + { + if (state.Analyzer.WasmModuleInfo is { } wasm) + return BuildWasmSectionsTable(ctx, state, wasm); + + return BuildPeSectionsTable(ctx, state); + } + + private static string GetPeSubTabLabel(DotsiderState state, int subTab) => + state.Analyzer.WasmModuleInfo is not null + ? subTab switch + { + PeSubTabId.Sections => "Sections", + PeSubTabId.TypeDef => "Types", + PeSubTabId.MethodDef => "Functions", + PeSubTabId.TypeRef => "Tables", + PeSubTabId.MemberRef => "Memories", + PeSubTabId.Attributes => "Globals", + PeSubTabId.Resources => "Data", + PeSubTabId.DebugDirectory => "Custom", + PeSubTabId.Imports => "Imports", + PeSubTabId.Exports => "Exports", + PeSubTabId.LoadConfig => "Elements", + PeSubTabId.RtrSections => "Tags", + PeSubTabId.AotTypes => "Module", + PeSubTabId.Symbols => "Symbols", + _ => "", + } + : subTab switch + { + PeSubTabId.Sections => "Sections", + PeSubTabId.TypeDef => "TypeDef", + PeSubTabId.MethodDef => "MethodDef", + PeSubTabId.TypeRef => "TypeRef", + PeSubTabId.MemberRef => "MemberRef", + PeSubTabId.Attributes => "Attributes", + PeSubTabId.Resources => "Resources", + PeSubTabId.DebugDirectory => "Debug Directory", + PeSubTabId.Imports => "Imports", + PeSubTabId.Exports => "Exports", + PeSubTabId.LoadConfig => "Load Config", + PeSubTabId.RtrSections => "R2R Sections", + PeSubTabId.AotTypes => "AOT Types", + PeSubTabId.Symbols => "Symbols", + _ => "", + }; + + private static TableWidget BuildWasmSectionsTable( + WidgetContext ctx, DotsiderState state, WasmModuleInfo wasm) + { + var query = state.Search[TabId.PeMetadata].Query; + var data = ApplySearch(wasm.Sections, query, s => $"{s.Id} {s.Name}"); + state.Search[TabId.PeMetadata].SetMatchCount(data.Count); + + return ctx.Table(data) + .RowKey(GetWasmSectionKey) + .Header(h => + [ + h.Cell("Id").Width(SizeHint.Fixed(6)), + h.Cell("Name").Width(SizeHint.Fill), + h.Cell("Payload Offset").Width(SizeHint.Fixed(16)), + h.Cell("Payload Size").Width(SizeHint.Fixed(14)) + ]) + .Row((r, s, rs) => + [ + r.Cell(c => FocusStyle(c, c.Text(s.Id.ToString()), rs.IsFocused)), + r.Cell(c => FocusHighlightCell(c, s.Name, query, true, rs.IsFocused)), + r.Cell(c => FocusStyle(c, HexCell(c, $"0x{s.FileOffset:X}"), rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(FormatSize(s.Size, state)), rs.IsFocused)) + ]) + .Focus(state.PeDetailContent is not null || state.App.FocusedNode is EditorNode ? null : state.PeFocusedKey) + .OnFocusChanged(key => state.PeFocusedKey = key) + .OnRowActivated((_, s) => + { + state.PeDetailContent = string.Join("\n", + "WebAssembly Section", + $"Id: {s.Id}", + $"Name: {s.Name}", + $"Payload Offset: 0x{s.FileOffset:X}", + $"Payload Size: {s.Size} (0x{s.Size:X})"); + state.App.RequestFocus(node => node is EditorNode); + state.App.Invalidate(); + }) + .Compact().Fill(); + } + + private static TableWidget BuildPeSectionsTable(WidgetContext ctx, DotsiderState state) { var query = state.Search[TabId.PeMetadata].Query; var data = ApplySearch(state.Analyzer.Sections, query, @@ -497,10 +610,13 @@ internal static IReadOnlyList GetDebugDirectoryRows(DotsiderS internal static string GetDebugDirectoryRowKey(DebugDirectoryRow row) => $"{row.Origin}:{GetDebugDirectoryKey(row.Info)}"; - private static TableWidget BuildDebugDirectoryTable( + private static Hex1bWidget BuildDebugDirectoryTable( WidgetContext ctx, DotsiderState state) { + if (state.Analyzer.WasmModuleInfo is { } wasm) + return BuildWasmCustomSectionsTable(ctx, state, wasm); + var query = state.Search[TabId.PeMetadata].Query; var merged = state.Analyzer.PreIlcCompanions is not null; var data = ApplySearch(GetDebugDirectoryRows(state), query, @@ -564,8 +680,11 @@ .. merged .Compact().Fill(); } - private static TableWidget BuildTypeDefsTable(WidgetContext ctx, DotsiderState state) + private static Hex1bWidget BuildTypeDefsTable(WidgetContext ctx, DotsiderState state) { + if (state.Analyzer.WasmModuleInfo is { } wasm) + return BuildWasmTypesTable(ctx, state, wasm); + var query = state.Search[TabId.PeMetadata].Query; var data = ApplySearch(state.MetadataAnalyzer.TypeDefs, query, t => $"{t.FullName} {t.BaseType} {t.Attributes}"); @@ -608,8 +727,11 @@ private static TableWidget BuildTypeDefsTable(WidgetContext BuildMethodDefsTable(WidgetContext ctx, DotsiderState state) + private static Hex1bWidget BuildMethodDefsTable(WidgetContext ctx, DotsiderState state) { + if (state.Analyzer.WasmModuleInfo is { } wasm) + return BuildWasmFunctionsTable(ctx, state, wasm); + var query = state.Search[TabId.PeMetadata].Query; var data = ApplySearch(state.MetadataAnalyzer.MethodDefs, query, m => $"{m.DeclaringType} {m.Name} {m.Signature}"); @@ -652,8 +774,11 @@ private static TableWidget BuildMethodDefsTable(WidgetContext BuildTypeRefsTable(WidgetContext ctx, DotsiderState state) + private static Hex1bWidget BuildTypeRefsTable(WidgetContext ctx, DotsiderState state) { + if (state.Analyzer.WasmModuleInfo is { } wasm) + return BuildWasmTablesTable(ctx, state, wasm); + var query = state.Search[TabId.PeMetadata].Query; var data = ApplySearch(state.MetadataAnalyzer.TypeRefs, query, t => $"{t.FullName} {t.ResolutionScope}"); @@ -689,8 +814,11 @@ private static TableWidget BuildTypeRefsTable(WidgetContext BuildMemberRefsTable(WidgetContext ctx, DotsiderState state) + private static Hex1bWidget BuildMemberRefsTable(WidgetContext ctx, DotsiderState state) { + if (state.Analyzer.WasmModuleInfo is { } wasm) + return BuildWasmMemoriesTable(ctx, state, wasm); + var query = state.Search[TabId.PeMetadata].Query; var data = ApplySearch(state.MetadataAnalyzer.MemberRefs, query, m => $"{m.DeclaringType} {m.Name}"); @@ -723,8 +851,11 @@ private static TableWidget BuildMemberRefsTable(WidgetContext BuildAttributesTable(WidgetContext ctx, DotsiderState state) + private static Hex1bWidget BuildAttributesTable(WidgetContext ctx, DotsiderState state) { + if (state.Analyzer.WasmModuleInfo is { } wasm) + return BuildWasmGlobalsTable(ctx, state, wasm); + var query = state.Search[TabId.PeMetadata].Query; var data = ApplySearch(state.MetadataAnalyzer.CustomAttributes, query, a => $"{a.Parent} {a.Constructor} {a.Value}"); @@ -759,8 +890,11 @@ private static TableWidget BuildAttributesTable(WidgetConte .Compact().Fill(); } - private static TableWidget BuildResourcesTable(WidgetContext ctx, DotsiderState state) + private static Hex1bWidget BuildResourcesTable(WidgetContext ctx, DotsiderState state) { + if (state.Analyzer.WasmModuleInfo is { } wasm) + return BuildWasmDataSegmentsTable(ctx, state, wasm); + var query = state.Search[TabId.PeMetadata].Query; var data = ApplySearch(state.MetadataAnalyzer.Resources, query, r => $"{r.Name} {r.Visibility}"); @@ -800,6 +934,335 @@ private static TableWidget BuildResourcesTable(WidgetContext BuildWasmTypesTable( + WidgetContext ctx, DotsiderState state, WasmModuleInfo wasm) + { + var query = state.Search[TabId.PeMetadata].Query; + var data = ApplySearch(wasm.Types, query, t => $"type {t.Index} {FormatWasmSignature(t.ParamTypes, t.ResultTypes)}"); + state.Search[TabId.PeMetadata].SetMatchCount(data.Count); + + return ctx.Table(data) + .RowKey(GetWasmTypeKey) + .Header(h => + [ + h.Cell("Index").Width(SizeHint.Fixed(8)), + h.Cell("Params").Width(SizeHint.Fill), + h.Cell("Results").Width(SizeHint.Fill) + ]) + .Row((r, t, rs) => + [ + r.Cell(c => FocusStyle(c, c.Text(t.Index.ToString()), rs.IsFocused)), + r.Cell(c => FocusHighlightCell(c, FormatWasmTypes(t.ParamTypes), query, true, rs.IsFocused)), + r.Cell(c => FocusHighlightCell(c, FormatWasmTypes(t.ResultTypes), query, true, rs.IsFocused)) + ]) + .Focus(state.PeDetailContent is not null || state.App.FocusedNode is EditorNode ? null : state.PeFocusedKey) + .OnFocusChanged(key => state.PeFocusedKey = key) + .OnRowActivated((_, t) => + { + state.PeDetailContent = string.Join("\n", + "WebAssembly Type", + $"Index: {t.Index}", + $"Signature: {FormatWasmSignature(t.ParamTypes, t.ResultTypes)}"); + state.App.RequestFocus(node => node is EditorNode); + state.App.Invalidate(); + }) + .Compact().Fill(); + } + + private static TableWidget BuildWasmFunctionsTable( + WidgetContext ctx, DotsiderState state, WasmModuleInfo wasm) + { + var query = state.Search[TabId.PeMetadata].Query; + var data = ApplySearch(wasm.Functions, query, + f => $"{f.Index} {f.Name} {f.NameSource} {f.ImportModule} {f.ImportName} {f.TypeIndex}"); + state.Search[TabId.PeMetadata].SetMatchCount(data.Count); + + return ctx.Table(data) + .RowKey(GetWasmFunctionKey) + .Header(h => + [ + h.Cell("Index").Width(SizeHint.Fixed(8)), + h.Cell("Name").Width(SizeHint.Fill), + h.Cell("Kind").Width(SizeHint.Fixed(9)), + h.Cell("Type").Width(SizeHint.Fixed(8)), + h.Cell("Offset").Width(SizeHint.Fixed(12)), + h.Cell("Size").Width(SizeHint.Fixed(10)) + ]) + .Row((r, f, rs) => + [ + r.Cell(c => FocusStyle(c, c.Text(f.Index.ToString()), rs.IsFocused)), + r.Cell(c => FocusHighlightCell(c, f.Name, query, true, rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(f.IsImported ? "import" : "defined"), rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(f.TypeIndex?.ToString() ?? ""), rs.IsFocused)), + r.Cell(c => FocusStyle(c, HexCell(c, f.CodeOffset is { } o ? $"0x{o:X}" : ""), rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(f.CodeSize > 0 ? FormatSize(f.CodeSize, state) : ""), rs.IsFocused)) + ]) + .Focus(state.PeDetailContent is not null || state.App.FocusedNode is EditorNode ? null : state.PeFocusedKey) + .OnFocusChanged(key => state.PeFocusedKey = key) + .OnRowActivated((_, f) => + { + state.PeDetailContent = string.Join("\n", + "WebAssembly Function", + $"Index: {f.Index}", + $"Name: {f.Name}", + $"Name Source: {f.NameSource}", + $"Kind: {(f.IsImported ? "import" : "defined")}", + $"Import: {(f.IsImported ? $"{f.ImportModule}!{f.ImportName}" : "(none)")}", + $"Type Index: {f.TypeIndex?.ToString() ?? "(none)"}", + $"Signature: {FormatWasmSignature(f.ParamTypes, f.ResultTypes)}", + $"Body Offset: {(f.BodyOffset is { } body ? $"0x{body:X}" : "(imported)")}", + $"Body Size: {f.BodySize}", + $"Code Offset: {(f.CodeOffset is { } code ? $"0x{code:X}" : "(imported)")}", + $"Code Size: {f.CodeSize}", + $"Exports: {(f.ExportNames.Count == 0 ? "(none)" : string.Join(", ", f.ExportNames))}"); + state.App.RequestFocus(node => node is EditorNode); + state.App.Invalidate(); + }) + .Compact().Fill(); + } + + private static TableWidget BuildWasmTablesTable( + WidgetContext ctx, DotsiderState state, WasmModuleInfo wasm) + { + var query = state.Search[TabId.PeMetadata].Query; + var data = ApplySearch(wasm.Tables, query, t => $"{t.Index} {t.RefType} {t.Minimum} {t.Maximum}"); + state.Search[TabId.PeMetadata].SetMatchCount(data.Count); + + return ctx.Table(data) + .RowKey(GetWasmTableKey) + .Header(h => + [ + h.Cell("Index").Width(SizeHint.Fixed(8)), + h.Cell("Ref Type").Width(SizeHint.Fill), + h.Cell("Minimum").Width(SizeHint.Fixed(12)), + h.Cell("Maximum").Width(SizeHint.Fixed(12)) + ]) + .Row((r, t, rs) => + [ + r.Cell(c => FocusStyle(c, c.Text(t.Index.ToString()), rs.IsFocused)), + r.Cell(c => FocusHighlightCell(c, t.RefType, query, true, rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(t.Minimum.ToString()), rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(t.Maximum?.ToString() ?? ""), rs.IsFocused)) + ]) + .Focus(state.PeDetailContent is not null || state.App.FocusedNode is EditorNode ? null : state.PeFocusedKey) + .OnFocusChanged(key => state.PeFocusedKey = key) + .Compact().Fill(); + } + + private static TableWidget BuildWasmMemoriesTable( + WidgetContext ctx, DotsiderState state, WasmModuleInfo wasm) + { + var query = state.Search[TabId.PeMetadata].Query; + var data = ApplySearch(wasm.Memories, query, m => $"{m.Index} {m.MinimumPages} {m.MaximumPages} {m.IsShared} {m.IsMemory64}"); + state.Search[TabId.PeMetadata].SetMatchCount(data.Count); + + return ctx.Table(data) + .RowKey(GetWasmMemoryKey) + .Header(h => + [ + h.Cell("Index").Width(SizeHint.Fixed(8)), + h.Cell("Min Pages").Width(SizeHint.Fixed(12)), + h.Cell("Max Pages").Width(SizeHint.Fixed(12)), + h.Cell("Shared").Width(SizeHint.Fixed(9)), + h.Cell("Memory64").Width(SizeHint.Fixed(10)) + ]) + .Row((r, m, rs) => + [ + r.Cell(c => FocusStyle(c, c.Text(m.Index.ToString()), rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(m.MinimumPages.ToString()), rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(m.MaximumPages?.ToString() ?? ""), rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(m.IsShared ? "yes" : "no"), rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(m.IsMemory64 ? "yes" : "no"), rs.IsFocused)) + ]) + .Focus(state.PeDetailContent is not null || state.App.FocusedNode is EditorNode ? null : state.PeFocusedKey) + .OnFocusChanged(key => state.PeFocusedKey = key) + .Compact().Fill(); + } + + private static TableWidget BuildWasmGlobalsTable( + WidgetContext ctx, DotsiderState state, WasmModuleInfo wasm) + { + var query = state.Search[TabId.PeMetadata].Query; + var data = ApplySearch(wasm.Globals, query, g => $"{g.Index} {g.ValueTypeName} {g.IsMutable}"); + state.Search[TabId.PeMetadata].SetMatchCount(data.Count); + + return ctx.Table(data) + .RowKey(GetWasmGlobalKey) + .Header(h => + [ + h.Cell("Index").Width(SizeHint.Fixed(8)), + h.Cell("Type").Width(SizeHint.Fill), + h.Cell("Mutable").Width(SizeHint.Fixed(10)) + ]) + .Row((r, g, rs) => + [ + r.Cell(c => FocusStyle(c, c.Text(g.Index.ToString()), rs.IsFocused)), + r.Cell(c => FocusHighlightCell(c, g.ValueTypeName, query, true, rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(g.IsMutable ? "yes" : "no"), rs.IsFocused)) + ]) + .Focus(state.PeDetailContent is not null || state.App.FocusedNode is EditorNode ? null : state.PeFocusedKey) + .OnFocusChanged(key => state.PeFocusedKey = key) + .Compact().Fill(); + } + + private static TableWidget BuildWasmDataSegmentsTable( + WidgetContext ctx, DotsiderState state, WasmModuleInfo wasm) + { + var query = state.Search[TabId.PeMetadata].Query; + var data = ApplySearch(wasm.DataSegments, query, d => $"{d.Index} {d.Mode} {d.FileOffset:X} {d.Size}"); + state.Search[TabId.PeMetadata].SetMatchCount(data.Count); + + return ctx.Table(data) + .RowKey(GetWasmDataSegmentKey) + .Header(h => + [ + h.Cell("Index").Width(SizeHint.Fixed(8)), + h.Cell("Mode").Width(SizeHint.Fill), + h.Cell("Offset").Width(SizeHint.Fixed(14)), + h.Cell("Size").Width(SizeHint.Fixed(12)) + ]) + .Row((r, d, rs) => + [ + r.Cell(c => FocusStyle(c, c.Text(d.Index.ToString()), rs.IsFocused)), + r.Cell(c => FocusHighlightCell(c, d.Mode, query, true, rs.IsFocused)), + r.Cell(c => FocusStyle(c, HexCell(c, $"0x{d.FileOffset:X}"), rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(FormatSize(d.Size, state)), rs.IsFocused)) + ]) + .Focus(state.PeDetailContent is not null || state.App.FocusedNode is EditorNode ? null : state.PeFocusedKey) + .OnFocusChanged(key => state.PeFocusedKey = key) + .Compact().Fill(); + } + + private static TableWidget BuildWasmCustomSectionsTable( + WidgetContext ctx, DotsiderState state, WasmModuleInfo wasm) + { + var query = state.Search[TabId.PeMetadata].Query; + var data = ApplySearch(GetWasmCustomSections(wasm), query, s => $"{s.Id} {s.Name} {s.FileOffset:X} {s.Size}"); + state.Search[TabId.PeMetadata].SetMatchCount(data.Count); + + return ctx.Table(data) + .RowKey(GetWasmSectionKey) + .Header(h => + [ + h.Cell("Name").Width(SizeHint.Fill), + h.Cell("Offset").Width(SizeHint.Fixed(14)), + h.Cell("Size").Width(SizeHint.Fixed(12)) + ]) + .Row((r, s, rs) => + [ + r.Cell(c => FocusHighlightCell(c, s.Name, query, true, rs.IsFocused)), + r.Cell(c => FocusStyle(c, HexCell(c, $"0x{s.FileOffset:X}"), rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(FormatSize(s.Size, state)), rs.IsFocused)) + ]) + .Focus(state.PeDetailContent is not null || state.App.FocusedNode is EditorNode ? null : state.PeFocusedKey) + .OnFocusChanged(key => state.PeFocusedKey = key) + .OnRowActivated((_, s) => + { + state.PeDetailContent = string.Join("\n", + "WebAssembly Custom Section", + $"Name: {s.Name}", + $"File Offset: 0x{s.FileOffset:X}", + $"Payload Size: {s.Size}"); + state.App.RequestFocus(node => node is EditorNode); + state.App.Invalidate(); + }) + .Compact().Fill(); + } + + private static TableWidget BuildWasmElementsTable( + WidgetContext ctx, DotsiderState state, WasmModuleInfo wasm) + { + var query = state.Search[TabId.PeMetadata].Query; + var data = ApplySearch(wasm.Elements, query, + e => $"{e.Index} {e.Mode} {e.TableIndex} {e.ElementType} {e.ElementCount}"); + state.Search[TabId.PeMetadata].SetMatchCount(data.Count); + + return ctx.Table(data) + .RowKey(GetWasmElementKey) + .Header(h => + [ + h.Cell("Index").Width(SizeHint.Fixed(8)), + h.Cell("Mode").Width(SizeHint.Fill), + h.Cell("Table").Width(SizeHint.Fixed(8)), + h.Cell("Type").Width(SizeHint.Fixed(14)), + h.Cell("Count").Width(SizeHint.Fixed(9)) + ]) + .Row((r, e, rs) => + [ + r.Cell(c => FocusStyle(c, c.Text(e.Index.ToString()), rs.IsFocused)), + r.Cell(c => FocusHighlightCell(c, e.Mode, query, true, rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(e.TableIndex?.ToString() ?? ""), rs.IsFocused)), + r.Cell(c => FocusHighlightCell(c, e.ElementType, query, true, rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(e.ElementCount.ToString()), rs.IsFocused)) + ]) + .Focus(state.PeDetailContent is not null || state.App.FocusedNode is EditorNode ? null : state.PeFocusedKey) + .OnFocusChanged(key => state.PeFocusedKey = key) + .OnRowActivated((_, e) => + { + state.PeDetailContent = string.Join("\n", + "WebAssembly Element Segment", + $"Index: {e.Index}", + $"Mode: {e.Mode}", + $"Table: {e.TableIndex?.ToString() ?? "(implicit)"}", + $"Element Type: {e.ElementType}", + $"Element Count: {e.ElementCount}"); + state.App.RequestFocus(node => node is EditorNode); + state.App.Invalidate(); + }) + .Compact().Fill(); + } + + private static TableWidget BuildWasmTagsTable( + WidgetContext ctx, DotsiderState state, WasmModuleInfo wasm) + { + var query = state.Search[TabId.PeMetadata].Query; + var data = ApplySearch(wasm.Tags, query, t => $"{t.Index} {t.Attribute} {t.TypeIndex}"); + state.Search[TabId.PeMetadata].SetMatchCount(data.Count); + + return ctx.Table(data) + .RowKey(GetWasmTagKey) + .Header(h => + [ + h.Cell("Index").Width(SizeHint.Fixed(8)), + h.Cell("Attribute").Width(SizeHint.Fill), + h.Cell("Type").Width(SizeHint.Fixed(8)) + ]) + .Row((r, t, rs) => + [ + r.Cell(c => FocusStyle(c, c.Text(t.Index.ToString()), rs.IsFocused)), + r.Cell(c => FocusHighlightCell(c, t.Attribute.ToString(), query, true, rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(t.TypeIndex.ToString()), rs.IsFocused)) + ]) + .Focus(state.PeDetailContent is not null || state.App.FocusedNode is EditorNode ? null : state.PeFocusedKey) + .OnFocusChanged(key => state.PeFocusedKey = key) + .Compact().Fill(); + } + + private static TableWidget BuildWasmModuleTable( + WidgetContext ctx, DotsiderState state, WasmModuleInfo wasm) + { + var query = state.Search[TabId.PeMetadata].Query; + var rows = GetWasmModuleRows(wasm); + var data = ApplySearch(rows, query, r => $"{r.Field} {r.Value}"); + state.Search[TabId.PeMetadata].SetMatchCount(data.Count); + + return ctx.Table(data) + .RowKey(r => GetWasmModuleInfoKey(r.Field)) + .Header(h => + [ + h.Cell("Field").Width(SizeHint.Fixed(24)), + h.Cell("Value").Width(SizeHint.Fill) + ]) + .Row((r, row, rs) => + [ + r.Cell(c => FocusHighlightCell(c, row.Field, query, true, rs.IsFocused)), + r.Cell(c => FocusHighlightCell(c, row.Value, query, true, rs.IsFocused)) + ]) + .Focus(state.PeDetailContent is not null || state.App.FocusedNode is EditorNode ? null : state.PeFocusedKey) + .OnFocusChanged(key => state.PeFocusedKey = key) + .Compact().Fill(); + } + private static string FormatSize(int size, DotsiderState state) => state.FormatSizeToggleable(size); @@ -849,31 +1312,57 @@ private static IReadOnlyList GetActiveRowKeys(DotsiderState state) var metadataAnalyzer = state.MetadataAnalyzer; return state.PeSubTab switch { + PeSubTabId.Sections when analyzer.WasmModuleInfo is { } wasm => [.. ApplySearch(wasm.Sections, query, + s => $"{s.Id} {s.Name}").Select(s => (object)GetWasmSectionKey(s))], PeSubTabId.Sections => [.. ApplySearch(analyzer.Sections, query, s => $"{s.Name} {s.Characteristics}").Select(s => (object)s.Name)], + PeSubTabId.TypeDef when analyzer.WasmModuleInfo is { } wasm => [.. ApplySearch(wasm.Types, query, + t => $"type {t.Index} {FormatWasmSignature(t.ParamTypes, t.ResultTypes)}").Select(t => (object)GetWasmTypeKey(t))], PeSubTabId.TypeDef => [.. ApplySearch(metadataAnalyzer.TypeDefs, query, t => $"{t.FullName} {t.BaseType} {t.Attributes}").Select(t => (object)t.Token)], + PeSubTabId.MethodDef when analyzer.WasmModuleInfo is { } wasm => [.. ApplySearch(wasm.Functions, query, + f => $"{f.Index} {f.Name} {f.NameSource} {f.ImportModule} {f.ImportName} {f.TypeIndex}").Select(f => (object)GetWasmFunctionKey(f))], PeSubTabId.MethodDef => [.. ApplySearch(metadataAnalyzer.MethodDefs, query, m => $"{m.DeclaringType} {m.Name} {m.Signature}").Select(m => (object)m.Token)], + PeSubTabId.TypeRef when analyzer.WasmModuleInfo is { } wasm => [.. ApplySearch(wasm.Tables, query, + t => $"{t.Index} {t.RefType} {t.Minimum} {t.Maximum}").Select(t => (object)GetWasmTableKey(t))], PeSubTabId.TypeRef => [.. ApplySearch(metadataAnalyzer.TypeRefs, query, t => $"{t.FullName} {t.ResolutionScope}").Select(t => (object)t.Token)], + PeSubTabId.MemberRef when analyzer.WasmModuleInfo is { } wasm => [.. ApplySearch(wasm.Memories, query, + m => $"{m.Index} {m.MinimumPages} {m.MaximumPages} {m.IsShared} {m.IsMemory64}").Select(m => (object)GetWasmMemoryKey(m))], PeSubTabId.MemberRef => [.. ApplySearch(metadataAnalyzer.MemberRefs, query, m => $"{m.DeclaringType} {m.Name}").Select(m => (object)m.Token)], + PeSubTabId.Attributes when analyzer.WasmModuleInfo is { } wasm => [.. ApplySearch(wasm.Globals, query, + g => $"{g.Index} {g.ValueTypeName} {g.IsMutable}").Select(g => (object)GetWasmGlobalKey(g))], PeSubTabId.Attributes => [.. ApplySearch(metadataAnalyzer.CustomAttributes, query, a => $"{a.Parent} {a.Constructor} {a.Value}").Select(a => (object)$"{a.Parent}|{a.Constructor}")], + PeSubTabId.Resources when analyzer.WasmModuleInfo is { } wasm => [.. ApplySearch(wasm.DataSegments, query, + d => $"{d.Index} {d.Mode} {d.FileOffset:X} {d.Size}").Select(d => (object)GetWasmDataSegmentKey(d))], PeSubTabId.Resources => [.. ApplySearch(metadataAnalyzer.Resources, query, r => $"{r.Name} {r.Visibility}").Select(r => (object)r.Name)], + PeSubTabId.DebugDirectory when analyzer.WasmModuleInfo is { } wasm => [.. ApplySearch(GetWasmCustomSections(wasm), query, + s => $"{s.Id} {s.Name} {s.FileOffset:X} {s.Size}").Select(s => (object)GetWasmSectionKey(s))], PeSubTabId.DebugDirectory => [.. ApplySearch(GetDebugDirectoryRows(state), query, r => $"{r.Origin} {r.Info.Type} {r.Info.Stamp:X8} {r.Info.Payload}").Select(r => (object)GetDebugDirectoryRowKey(r))], + PeSubTabId.Imports when analyzer.WasmModuleInfo is { } wasm => [.. ApplySearch(wasm.Imports, query, + i => $"{i.ModuleName} {i.Name} {i.Kind}").Select(i => (object)GetWasmImportKey(i))], PeSubTabId.Imports => [.. ApplySearch(GetImportRows(analyzer), query, r => $"{r.Module} {r.Function.Name}").Select(r => (object)r.Key)], + PeSubTabId.Exports when analyzer.WasmModuleInfo is { } wasm => [.. ApplySearch(wasm.Exports, query, + e => $"{e.Name} {e.Kind} {e.Index}").Select(e => (object)GetWasmExportKey(e))], PeSubTabId.Exports => [.. ApplySearch(analyzer.Exports, query, e => $"{e.Name} {e.Ordinal} {e.ForwardedTo}").Select(e => (object)e.Ordinal)], + PeSubTabId.LoadConfig when analyzer.WasmModuleInfo is { } wasm => [.. ApplySearch(wasm.Elements, query, + e => $"{e.Index} {e.Mode} {e.TableIndex} {e.ElementType} {e.ElementCount}").Select(e => (object)GetWasmElementKey(e))], PeSubTabId.LoadConfig when analyzer.LoadConfig is not null => [.. ApplySearch(GetLoadConfigRows(analyzer.LoadConfig), query, r => $"{r.Field} {r.Value}").Select(r => (object)r.Field)], + PeSubTabId.RtrSections when analyzer.WasmModuleInfo is { } wasm => [.. ApplySearch(wasm.Tags, query, + t => $"{t.Index} {t.Attribute} {t.TypeIndex}").Select(t => (object)GetWasmTagKey(t))], PeSubTabId.RtrSections => [.. ApplySearch(analyzer.ReadyToRunSections, query, s => $"{s.SectionId} {s.Name}").Select(s => (object)s.SectionId)], + PeSubTabId.AotTypes when analyzer.WasmModuleInfo is { } wasm => [.. ApplySearch(GetWasmModuleRows(wasm), query, + r => $"{r.Field} {r.Value}").Select(r => (object)GetWasmModuleInfoKey(r.Field))], PeSubTabId.AotTypes => [.. ApplySearch(analyzer.RecoveredTypes, query, t => t.FullName).Select(t => (object)t.FullName)], PeSubTabId.Symbols => [.. ApplySearch(GetSymbolRows(analyzer), query, @@ -882,7 +1371,58 @@ [.. ApplySearch(GetLoadConfigRows(analyzer.LoadConfig), query, }; } - private static TableWidget BuildImportsTable( + private static Hex1bWidget BuildImportsTable( + WidgetContext ctx, DotsiderState state) + { + if (state.Analyzer.WasmModuleInfo is { } wasm) + return BuildWasmImportsTable(ctx, state, wasm); + + return BuildPeImportsTable(ctx, state); + } + + private static TableWidget BuildWasmImportsTable( + WidgetContext ctx, DotsiderState state, WasmModuleInfo wasm) + { + var query = state.Search[TabId.PeMetadata].Query; + var data = ApplySearch(wasm.Imports, query, i => $"{i.ModuleName} {i.Name} {i.Kind}"); + state.Search[TabId.PeMetadata].SetMatchCount(data.Count); + + return ctx.Table(data) + .RowKey(GetWasmImportKey) + .Header(h => + [ + h.Cell("Kind").Width(SizeHint.Fixed(10)), + h.Cell("Index").Width(SizeHint.Fixed(8)), + h.Cell("Module").Width(SizeHint.Fixed(26)), + h.Cell("Name").Width(SizeHint.Fill), + h.Cell("Type").Width(SizeHint.Fixed(8)) + ]) + .Row((r, i, rs) => + [ + r.Cell(c => FocusStyle(c, c.Text(i.Kind.ToString()), rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(i.Index.ToString()), rs.IsFocused)), + r.Cell(c => FocusHighlightCell(c, i.ModuleName, query, true, rs.IsFocused)), + r.Cell(c => FocusHighlightCell(c, i.Name, query, true, rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(i.TypeIndex?.ToString() ?? ""), rs.IsFocused)) + ]) + .Focus(state.PeDetailContent is not null || state.App.FocusedNode is EditorNode ? null : state.PeFocusedKey) + .OnFocusChanged(key => state.PeFocusedKey = key) + .OnRowActivated((_, i) => + { + state.PeDetailContent = string.Join("\n", + "WebAssembly Import", + $"Kind: {i.Kind}", + $"Index: {i.Index}", + $"Module: {i.ModuleName}", + $"Name: {i.Name}", + $"Type Index: {i.TypeIndex?.ToString() ?? "(none)"}"); + state.App.RequestFocus(node => node is EditorNode); + state.App.Invalidate(); + }) + .Compact().Fill(); + } + + private static TableWidget BuildPeImportsTable( WidgetContext ctx, DotsiderState state) { var query = state.Search[TabId.PeMetadata].Query; @@ -922,7 +1462,52 @@ private static TableWidget BuildImportsTable( .Compact().Fill(); } - private static TableWidget BuildExportsTable( + private static Hex1bWidget BuildExportsTable( + WidgetContext ctx, DotsiderState state) + { + if (state.Analyzer.WasmModuleInfo is { } wasm) + return BuildWasmExportsTable(ctx, state, wasm); + + return BuildPeExportsTable(ctx, state); + } + + private static TableWidget BuildWasmExportsTable( + WidgetContext ctx, DotsiderState state, WasmModuleInfo wasm) + { + var query = state.Search[TabId.PeMetadata].Query; + var data = ApplySearch(wasm.Exports, query, e => $"{e.Name} {e.Kind} {e.Index}"); + state.Search[TabId.PeMetadata].SetMatchCount(data.Count); + + return ctx.Table(data) + .RowKey(GetWasmExportKey) + .Header(h => + [ + h.Cell("Kind").Width(SizeHint.Fixed(10)), + h.Cell("Index").Width(SizeHint.Fixed(8)), + h.Cell("Name").Width(SizeHint.Fill) + ]) + .Row((r, e, rs) => + [ + r.Cell(c => FocusStyle(c, c.Text(e.Kind.ToString()), rs.IsFocused)), + r.Cell(c => FocusStyle(c, c.Text(e.Index.ToString()), rs.IsFocused)), + r.Cell(c => FocusHighlightCell(c, e.Name, query, true, rs.IsFocused)) + ]) + .Focus(state.PeDetailContent is not null || state.App.FocusedNode is EditorNode ? null : state.PeFocusedKey) + .OnFocusChanged(key => state.PeFocusedKey = key) + .OnRowActivated((_, e) => + { + state.PeDetailContent = string.Join("\n", + "WebAssembly Export", + $"Kind: {e.Kind}", + $"Index: {e.Index}", + $"Name: {e.Name}"); + state.App.RequestFocus(node => node is EditorNode); + state.App.Invalidate(); + }) + .Compact().Fill(); + } + + private static TableWidget BuildPeExportsTable( WidgetContext ctx, DotsiderState state) { var query = state.Search[TabId.PeMetadata].Query; @@ -962,9 +1547,12 @@ private static TableWidget BuildExportsTable( .Compact().Fill(); } - private static TableWidget BuildLoadConfigTable( + private static Hex1bWidget BuildLoadConfigTable( WidgetContext ctx, DotsiderState state) { + if (state.Analyzer.WasmModuleInfo is { } wasm) + return BuildWasmElementsTable(ctx, state, wasm); + var query = state.Search[TabId.PeMetadata].Query; IReadOnlyList rows = state.Analyzer.LoadConfig is { } loadConfig ? GetLoadConfigRows(loadConfig) @@ -997,9 +1585,12 @@ private static TableWidget BuildLoadConfigTable( .Compact().Fill(); } - private static TableWidget BuildRtrSectionsTable( + private static Hex1bWidget BuildRtrSectionsTable( WidgetContext ctx, DotsiderState state) { + if (state.Analyzer.WasmModuleInfo is { } wasm) + return BuildWasmTagsTable(ctx, state, wasm); + var query = state.Search[TabId.PeMetadata].Query; var data = ApplySearch(state.Analyzer.ReadyToRunSections, query, s => $"{s.SectionId} {s.Name}"); state.Search[TabId.PeMetadata].SetMatchCount(data.Count); @@ -1039,9 +1630,12 @@ private static TableWidget BuildRtrSectionsTable( .Compact().Fill(); } - private static TableWidget BuildAotTypesTable( + private static Hex1bWidget BuildAotTypesTable( WidgetContext ctx, DotsiderState state) { + if (state.Analyzer.WasmModuleInfo is { } wasm) + return BuildWasmModuleTable(ctx, state, wasm); + var query = state.Search[TabId.PeMetadata].Query; var data = ApplySearch(state.Analyzer.RecoveredTypes, query, t => t.FullName); state.Search[TabId.PeMetadata].SetMatchCount(data.Count); @@ -1156,12 +1750,123 @@ private static IReadOnlyList GetLoadConfigRows(LoadConfigInfo loa private static string ImportFunctionDisplay(ImportedFunctionInfo function) => function.Name ?? $"#{function.Ordinal}"; + /// Builds a stable key for a WebAssembly section row. + private static string GetWasmSectionKey(WasmSectionInfo section) => + $"{section.FileOffset}:{section.Id}:{section.Name}"; + + /// Builds a stable key for a WebAssembly type row. + private static string GetWasmTypeKey(WasmTypeInfo type) => + $"wasm:type:{type.Index}"; + + /// Builds a stable key for a WebAssembly function row. + private static string GetWasmFunctionKey(WasmFunctionInfo function) => + $"wasm:function:{function.Index}"; + + /// Builds a stable key for a WebAssembly table row. + private static string GetWasmTableKey(WasmTableInfo table) => + $"wasm:table:{table.Index}"; + + /// Builds a stable key for a WebAssembly memory row. + private static string GetWasmMemoryKey(WasmMemoryInfo memory) => + $"wasm:memory:{memory.Index}"; + + /// Builds a stable key for a WebAssembly global row. + private static string GetWasmGlobalKey(WasmGlobalInfo global) => + $"wasm:global:{global.Index}"; + + /// Builds a stable key for a WebAssembly data-segment row. + private static string GetWasmDataSegmentKey(WasmDataSegmentInfo dataSegment) => + $"wasm:data:{dataSegment.Index}"; + + /// Builds a stable key for a WebAssembly import row. + private static string GetWasmImportKey(WasmImportInfo import) => + $"{import.Kind}:{import.Index}:{import.ModuleName}:{import.Name}"; + + /// Builds a stable key for a WebAssembly export row. + private static string GetWasmExportKey(WasmExportInfo export) => + $"{export.Kind}:{export.Index}:{export.Name}"; + + /// Builds a stable key for a WebAssembly element-segment row. + private static string GetWasmElementKey(WasmElementSegmentInfo element) => + $"wasm:element:{element.Index}"; + + /// Builds a stable key for a WebAssembly tag row. + private static string GetWasmTagKey(WasmTagInfo tag) => + $"wasm:tag:{tag.Index}"; + + /// Builds a stable key for a WebAssembly module-summary row. + private static string GetWasmModuleInfoKey(string field) => + $"wasm:module:{field}"; + + /// Returns custom sections for the WebAssembly custom-section table. + private static IReadOnlyList GetWasmCustomSections(WasmModuleInfo wasm) => + [.. wasm.Sections.Where(static s => s.Id == 0)]; + + /// Projects module-level WebAssembly facts into field/value rows. + private static IReadOnlyList GetWasmModuleRows(WasmModuleInfo wasm) => + [ + new("Version", wasm.Version.ToString()), + new("Sections", wasm.Sections.Count.ToString()), + new("Types", wasm.Types.Count.ToString()), + new("Functions", $"{wasm.Functions.Count} ({wasm.ImportedFunctionCount} imported, {wasm.DefinedFunctionCount} defined)"), + new("Tables", wasm.Tables.Count.ToString()), + new("Memories", wasm.Memories.Count.ToString()), + new("Globals", wasm.Globals.Count.ToString()), + new("Elements", wasm.Elements.Count.ToString()), + new("Data", $"{wasm.DataSegments.Count} segments, {wasm.DataSize} bytes"), + new("Tags", wasm.Tags.Count.ToString()), + new("Start", wasm.StartFunctionIndex?.ToString() ?? "(none)"), + new("Data Count", wasm.DataCount?.ToString() ?? "(none)"), + new("Symbol Map", wasm.SymbolMapStatus.ToString()), + new("Symbol Map Entries", wasm.SymbolMapEntryCount.ToString()), + new("Symbol Map Path", wasm.SymbolMapPath ?? "(none)"), + new("Target Features", wasm.TargetFeatures.Count == 0 ? "(none)" : string.Join(", ", wasm.TargetFeatures)), + new("Producers", wasm.ProducerFields.Count == 0 ? "(none)" : string.Join(", ", wasm.ProducerFields)), + new("Diagnostic", wasm.Diagnostic ?? "(none)") + ]; + + /// Formats a WebAssembly function signature for display. + private static string FormatWasmSignature(IReadOnlyList paramTypes, IReadOnlyList resultTypes) => + $"({FormatWasmTypes(paramTypes)}) -> {FormatWasmTypes(resultTypes)}"; + + /// Formats WebAssembly value-type bytes for display. + private static string FormatWasmTypes(IReadOnlyList valueTypes) => + valueTypes.Count == 0 + ? "void" + : string.Join(", ", valueTypes.Select(FormatWasmType)); + + /// Formats a WebAssembly value-type byte for display. + private static string FormatWasmType(byte valueType) => + valueType switch + { + 0x7F => "i32", + 0x7E => "i64", + 0x7D => "f32", + 0x7C => "f64", + 0x7B => "v128", + 0x70 => "funcref", + 0x6F => "externref", + 0x68 => "i31ref", + 0x67 => "structref", + 0x66 => "arrayref", + 0x64 => "exnref", + 0x63 => "anyref", + 0x62 => "eqref", + 0x6C => "nullexternref", + 0x6D => "nullref", + 0x6E => "nullfuncref", + _ => $"0x{valueType:X2}" + }; + /// A single imported function flattened with its owning module. private sealed record ImportRow(string Module, ImportedFunctionInfo Function, string Key); /// A load-configuration field projected for table display. private sealed record LoadConfigRow(string Field, string Value); + /// A WebAssembly module fact projected for table display. + private sealed record WasmModuleRow(string Field, string Value); + private static string GetDebugDirectoryKey(DebugDirectoryInfo info) => $"{info.Type}:{info.AddressOfRawData:X8}:{info.PointerToRawData:X8}"; diff --git a/tests/Dotsider.Mcp.Tests/AssemblyToolsTests.cs b/tests/Dotsider.Mcp.Tests/AssemblyToolsTests.cs index 3096e6d0..da2820b0 100644 --- a/tests/Dotsider.Mcp.Tests/AssemblyToolsTests.cs +++ b/tests/Dotsider.Mcp.Tests/AssemblyToolsTests.cs @@ -260,6 +260,30 @@ public async Task ListTypes_EmptyLib_ReturnsMinimalTypes() Assert.Equal(JsonValueKind.Array, types.ValueKind); } + /// + /// list_types unwraps Webcil browser app assemblies and returns their managed type definitions. + /// + [Fact(Timeout = 30_000)] + public async Task ListTypes_WebcilWasm_ReturnsManagedTypes() + { + Assert.SkipWhen(samples.WasmConsoleWebcilWasm is null, + "browser-wasm publish did not produce the Webcil app assembly on this leg."); + + await StartServerAsync(); + await using var client = await CreateClientAsync(); + + var result = await client.CallToolAsync( + "list_types", + new Dictionary { ["assemblyPath"] = samples.WasmConsoleWebcilWasm }, + cancellationToken: TestCancellationToken); + + var text = GetTextContent(result); + Assert.NotNull(text); + var types = JsonSerializer.Deserialize(text); + Assert.Contains(types.EnumerateArray(), static type => + type.GetProperty("fullName").GetString() == "WasmCalculator"); + } + /// /// get_assembly_info exposes displayName, bundle flags, and preferred runtime pack. /// @@ -309,6 +333,67 @@ public async Task GetAssemblyInfo_NativeAot_ReportsBinaryKindAndRtr() Assert.False(json.GetProperty("hasMetadata").GetBoolean()); } + /// + /// get_assembly_info reports a raw dotnet.native.wasm module as WebAssembly and includes + /// function, code, data, and symbol-map facts from the SDK-produced module. + /// + [Fact(Timeout = 30_000)] + public async Task GetAssemblyInfo_Wasm_ReportsModuleFacts() + { + var wasmPath = GetWasmNativePath(); + + await StartServerAsync(); + await using var client = await CreateClientAsync(); + + var result = await client.CallToolAsync("get_assembly_info", + new Dictionary { ["assemblyPath"] = wasmPath }, + cancellationToken: TestCancellationToken); + + var text = GetTextContent(result); + Assert.NotNull(text); + var json = JsonSerializer.Deserialize(text); + Assert.Equal("wasm", json.GetProperty("binaryKind").GetString()); + Assert.Equal("Wasm32", json.GetProperty("architecture").GetString()); + Assert.False(json.GetProperty("hasMetadata").GetBoolean()); + var wasm = json.GetProperty("wasm"); + Assert.True(wasm.GetProperty("definedFunctionCount").GetInt32() > 0); + Assert.True(wasm.GetProperty("importedFunctionCount").GetInt32() > 0); + Assert.True(wasm.GetProperty("codeSize").GetInt64() > 0); + Assert.True(wasm.GetProperty("dataSize").GetInt64() > 0); + Assert.Equal("Loaded", wasm.GetProperty("symbolMapStatus").GetString()); + } + + /// + /// get_assembly_info reports a Webcil-wrapped browser app assembly as normal managed metadata + /// with Webcil payload facts attached, not as the raw runtime WebAssembly module. + /// + [Fact(Timeout = 30_000)] + public async Task GetAssemblyInfo_WebcilWasm_ReportsManagedMetadata() + { + Assert.SkipWhen(samples.WasmConsoleWebcilWasm is null, + "browser-wasm publish did not produce the Webcil app assembly on this leg."); + + await StartServerAsync(); + await using var client = await CreateClientAsync(); + + var result = await client.CallToolAsync("get_assembly_info", + new Dictionary { ["assemblyPath"] = samples.WasmConsoleWebcilWasm }, + cancellationToken: TestCancellationToken); + + var text = GetTextContent(result); + Assert.NotNull(text); + var json = JsonSerializer.Deserialize(text); + Assert.Equal("managed", json.GetProperty("binaryKind").GetString()); + Assert.Equal("Wasm32", json.GetProperty("architecture").GetString()); + Assert.True(json.GetProperty("hasMetadata").GetBoolean()); + Assert.Equal("WasmConsole", json.GetProperty("assemblyName").GetString()); + Assert.False(json.TryGetProperty("wasm", out _)); + var webcil = json.GetProperty("webcil"); + Assert.True(webcil.GetProperty("isWasmWrapped").GetBoolean()); + Assert.True(webcil.GetProperty("sectionCount").GetInt32() > 0); + Assert.True(webcil.GetProperty("metadataSize").GetInt32() > 0); + } + /// /// get_assembly_info reports a managed assembly's binary kind as managed with /// no Native AOT info attached. @@ -472,4 +557,12 @@ public async Task FindMembers_NativeAot_SearchesRecoveredInventory() || json.GetProperty("methods").GetArrayLength() > 0); Assert.Equal(0, json.GetProperty("memberRefs").GetArrayLength()); } + + private string GetWasmNativePath() + { + Assert.SkipWhen(samples.WasmConsoleNativeWasm is null && samples.ReadyToRunConsoleWasmNativeWasm is null, + "browser-wasm publish did not run on this leg."); + + return samples.WasmConsoleNativeWasm ?? samples.ReadyToRunConsoleWasmNativeWasm!; + } } diff --git a/tests/Dotsider.Mcp.Tests/SampleAssemblyFixture.cs b/tests/Dotsider.Mcp.Tests/SampleAssemblyFixture.cs index 5b74cbf4..d7f05c95 100644 --- a/tests/Dotsider.Mcp.Tests/SampleAssemblyFixture.cs +++ b/tests/Dotsider.Mcp.Tests/SampleAssemblyFixture.cs @@ -100,6 +100,25 @@ public class SampleAssemblyFixture : IAsyncLifetime /// public string? ReadyToRunConsoleDll { get; private set; } + /// + /// Path to the SDK-produced browser Wasm runtime module when the wasm-tools workload is present. + /// MCP symbol and assembly-info tests gate on it because not every developer machine has the + /// workload installed. + /// + public string? ReadyToRunConsoleWasmNativeWasm { get; private set; } + + /// + /// Path to the dedicated SDK-produced browser Wasm runtime module from samples/WasmConsole. + /// MCP raw Wasm tests prefer this focused fixture when the wasm-tools workload is installed. + /// + public string? WasmConsoleNativeWasm { get; private set; } + + /// + /// Path to the Webcil-wrapped managed app assembly from samples/WasmConsole. + /// MCP managed Webcil tests use this to verify normal metadata tools still work. + /// + public string? WasmConsoleWebcilWasm { get; private set; } + /// /// Builds all sample projects once per collection and resolves their output paths. /// @@ -120,6 +139,8 @@ public async ValueTask InitializeAsync() await PublishNativeAotProject("samples/NativeAotConsole"); await PublishNativeAotProject("samples/NativeAotConsoleV2"); await PublishReadyToRunProject("samples/ReadyToRunConsole"); + await PublishWasmProject("samples/ReadyToRunConsole"); + await PublishWasmProject("samples/WasmConsole"); const string config = "Debug"; const string tfm = "net10.0"; @@ -146,6 +167,15 @@ public async ValueTask InitializeAsync() var r2rConsoleDll = Path.Combine(_repoRoot, "samples", "ReadyToRunConsole", "bin", "Release", tfm, rid, "publish", "ReadyToRunConsole.dll"); ReadyToRunConsoleDll = File.Exists(r2rConsoleDll) ? r2rConsoleDll : null; + var wasmNative = Path.Combine(_repoRoot, "samples", "ReadyToRunConsole", + "bin", "Release", tfm, "browser-wasm", "publish", "dotnet.native.wasm"); + ReadyToRunConsoleWasmNativeWasm = File.Exists(wasmNative) ? wasmNative : null; + var wasmConsoleNative = Path.Combine(_repoRoot, "samples", "WasmConsole", + "bin", "Release", tfm, "browser-wasm", "publish", "dotnet.native.wasm"); + WasmConsoleNativeWasm = File.Exists(wasmConsoleNative) ? wasmConsoleNative : null; + var wasmConsoleWebcil = Path.Combine(_repoRoot, "samples", "WasmConsole", + "bin", "Release", tfm, "browser-wasm", "AppBundle", "_framework", "WasmConsole.wasm"); + WasmConsoleWebcilWasm = File.Exists(wasmConsoleWebcil) ? wasmConsoleWebcil : null; Assert.True(File.Exists(HelloWorldDll), $"HelloWorld.dll not found at {HelloWorldDll}"); Assert.True(File.Exists(HelloWorldExe), $"HelloWorld apphost not found at {HelloWorldExe}"); @@ -372,6 +402,61 @@ private async Task PublishReadyToRunProject(string relativePath) } } + private async Task PublishWasmProject(string relativePath) + { + var expectedOutput = Path.Combine(_repoRoot, relativePath, + "bin", "Release", "net10.0", "browser-wasm", "publish", "dotnet.native.wasm"); + + if (File.Exists(expectedOutput)) + return; + + var lockName = "dotsider-build-" + relativePath.Replace('/', '-').Replace('\\', '-') + "-browser-wasm.lock"; + var lockPath = Path.Combine(Path.GetTempPath(), lockName); + + FileStream lockFile; + while (true) + { + try + { + lockFile = new FileStream(lockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + break; + } + catch (IOException) + { + await Task.Delay(200); + } + } + + try + { + if (File.Exists(expectedOutput)) + { + lockFile.Dispose(); + return; + } + + var psi = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "publish -c Release -r browser-wasm --self-contained true " + + "-p:PublishReadyToRun=false -v q", + WorkingDirectory = Path.Combine(_repoRoot, relativePath), + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + var process = Process.Start(psi)!; + _ = await process.StandardOutput.ReadToEndAsync(); + _ = await process.StandardError.ReadToEndAsync(); + await process.WaitForExitAsync(); + // A non-zero exit means wasm-tools is unavailable; the output stays absent. + } + finally + { + lockFile.Dispose(); + } + } + private async Task PublishNativeAotProject(string relativePath) { var rid = RuntimeInformation.RuntimeIdentifier; diff --git a/tests/Dotsider.Mcp.Tests/SymbolToolsTests.cs b/tests/Dotsider.Mcp.Tests/SymbolToolsTests.cs index bba02e65..6fed4ae2 100644 --- a/tests/Dotsider.Mcp.Tests/SymbolToolsTests.cs +++ b/tests/Dotsider.Mcp.Tests/SymbolToolsTests.cs @@ -1,3 +1,8 @@ +using Dotsider.Core.Analysis; +using Dotsider.Core.Analysis.Disasm; +using Dotsider.Core.Analysis.Models; +using System.Text.Json; + namespace Dotsider.Mcp.Tests; /// @@ -53,7 +58,7 @@ public async Task GetNativeSymbols_Managed_ReturnsError() } /// get_native_disassembly decodes a function by address into structured instructions. - [Fact(Timeout = 60_000)] + [Fact(Timeout = 30_000)] public async Task GetNativeDisassembly_NativeAot_ByAddress_ReturnsInstructions() { Assert.SkipWhen(samples.NativeAotConsoleExe is null, "NativeAOT sample was not built"); @@ -82,6 +87,105 @@ public async Task GetNativeDisassembly_NativeAot_ByAddress_ReturnsInstructions() Assert.Contains("\"mnemonic\"", text); } + /// + /// get_native_symbols returns WebAssembly function symbols for a raw SDK browser-wasm module. + /// + [Fact(Timeout = 30_000)] + public async Task GetNativeSymbols_Wasm_ReturnsWebAssemblySymbols() + { + var wasmPath = GetWasmNativePath(); + + await StartServerAsync(); + await using var client = await CreateClientAsync(); + + var result = await client.CallToolAsync( + "get_native_symbols", + new Dictionary { ["assemblyPath"] = wasmPath }, + cancellationToken: TestCancellationToken); + + var text = GetTextContent(result); + Assert.NotNull(text); + var json = JsonSerializer.Deserialize(text); + Assert.Equal("webAssembly", json.GetProperty("source").GetString()); + Assert.Equal("wasm32", json.GetProperty("architecture").GetString()); + Assert.True(json.GetProperty("symbols").GetArrayLength() > 0); + } + + /// + /// get_native_disassembly decodes a WebAssembly function from a real dotnet.native.wasm + /// module through the same native-tool surface used for PE, ELF, Mach-O, and ReadyToRun code. + /// + [Fact(Timeout = 30_000)] + public async Task GetNativeDisassembly_Wasm_ByAddress_ReturnsInstructions() + { + var wasmPath = GetWasmNativePath(); + + string address; + using (var analyzer = new AssemblyAnalyzer(wasmPath)) + { + var symbol = FindWasmFunctionWithNamedCall(analyzer); + address = $"0x{symbol.VirtualAddress:x}"; + } + + await StartServerAsync(); + await using var client = await CreateClientAsync(); + + var result = await client.CallToolAsync( + "get_native_disassembly", + new Dictionary + { + ["assemblyPath"] = wasmPath, + ["address"] = address + }, + cancellationToken: TestCancellationToken); + + var text = GetTextContent(result); + Assert.NotNull(text); + Assert.False(text.StartsWith("Error", StringComparison.Ordinal), text); + var json = JsonSerializer.Deserialize(text); + Assert.Equal("Wasm32", json.GetProperty("architecture").GetString()); + Assert.Contains(json.GetProperty("instructions").EnumerateArray(), static instruction => + instruction.TryGetProperty("targetName", out var targetName) + && targetName.ValueKind == JsonValueKind.String); + } + + /// + /// get_native_disassembly accepts WebAssembly func:N identifiers through the same + /// symbolName parameter used for PE, ELF, Mach-O, ReadyToRun, and Native AOT symbols. + /// + [Fact(Timeout = 30_000)] + public async Task GetNativeDisassembly_Wasm_ByFunctionIndex_ReturnsInstructions() + { + var wasmPath = GetWasmNativePath(); + + string funcAlias; + using (var analyzer = new AssemblyAnalyzer(wasmPath)) + { + var symbol = analyzer.NativeSymbols!.Symbols.First(s => + s.Aliases.Any(static alias => alias.StartsWith("func:", StringComparison.Ordinal))); + funcAlias = symbol.Aliases.First(static alias => alias.StartsWith("func:", StringComparison.Ordinal)); + } + + await StartServerAsync(); + await using var client = await CreateClientAsync(); + + var result = await client.CallToolAsync( + "get_native_disassembly", + new Dictionary + { + ["assemblyPath"] = wasmPath, + ["symbolName"] = funcAlias + }, + cancellationToken: TestCancellationToken); + + var text = GetTextContent(result); + Assert.NotNull(text); + Assert.False(text.StartsWith("Error", StringComparison.Ordinal), text); + var json = JsonSerializer.Deserialize(text); + Assert.Equal("Wasm32", json.GetProperty("architecture").GetString()); + Assert.True(json.GetProperty("instructions").GetArrayLength() > 0); + } + /// get_native_disassembly reports an error for a managed assembly. [Fact(Timeout = 30_000)] public async Task GetNativeDisassembly_Managed_ReturnsError() @@ -121,4 +225,29 @@ public async Task GetAssemblyInfo_NativeAot_CarriesSymbolProvenance() Assert.Contains("nativeSymbolSource", text); Assert.Contains("nativeSymbolStatus", text); } + + private static NativeSymbol FindWasmFunctionWithNamedCall(AssemblyAnalyzer analyzer) + { + var info = analyzer.NativeSymbols; + Assert.NotNull(info); + foreach (var symbol in info.Symbols.Take(512)) + { + var result = NativeDisassembler.DisassembleSymbol(analyzer, symbol); + if (result is null) + continue; + + if (result.Value.Instructions.Any(static instruction => instruction.TargetName is not null)) + return symbol; + } + + throw new InvalidOperationException("No Wasm function with a named direct call was found."); + } + + private string GetWasmNativePath() + { + Assert.SkipWhen(samples.WasmConsoleNativeWasm is null && samples.ReadyToRunConsoleWasmNativeWasm is null, + "browser-wasm publish did not run on this leg."); + + return samples.WasmConsoleNativeWasm ?? samples.ReadyToRunConsoleWasmNativeWasm!; + } } diff --git a/tests/Dotsider.Mcp.Tests/ToolRegistrationTests.cs b/tests/Dotsider.Mcp.Tests/ToolRegistrationTests.cs index 7db7f12b..4d771087 100644 --- a/tests/Dotsider.Mcp.Tests/ToolRegistrationTests.cs +++ b/tests/Dotsider.Mcp.Tests/ToolRegistrationTests.cs @@ -54,6 +54,10 @@ public async Task ListTools_ReturnsAllRegisteredTools() Assert.Contains("get_native_symbols", names); Assert.Contains("get_native_disassembly", names); + // WebAssembly tools + Assert.Contains("list_wasm_sections", names); + Assert.Contains("list_wasm_functions", names); + // Native AOT tools Assert.Contains("get_native_aot_info", names); Assert.Contains("list_native_aot_sections", names); diff --git a/tests/Dotsider.Mcp.Tests/WasmToolsTests.cs b/tests/Dotsider.Mcp.Tests/WasmToolsTests.cs new file mode 100644 index 00000000..380924f2 --- /dev/null +++ b/tests/Dotsider.Mcp.Tests/WasmToolsTests.cs @@ -0,0 +1,96 @@ +using Dotsider.Core.Protocol; +using System.Text.Json; + +namespace Dotsider.Mcp.Tests; + +/// +/// Tests for the MCP WebAssembly tools over real SDK-produced browser-wasm output. +/// +[Collection("SampleAssemblies")] +public sealed class WasmToolsTests(SampleAssemblyFixture samples) : McpServerTestBase +{ + /// + /// list_wasm_sections returns raw Wasm section payload offsets and sizes from dotnet.native.wasm. + /// + [Fact(Timeout = 30_000)] + public async Task ListWasmSections_Direct_ReturnsSections() + { + var wasmPath = GetWasmNativePath(); + + await StartServerAsync(); + await using var client = await CreateClientAsync(); + + var result = await client.CallToolAsync( + "list_wasm_sections", + new Dictionary { ["assemblyPath"] = wasmPath }, + cancellationToken: TestCancellationToken); + + var text = GetTextContent(result); + Assert.NotNull(text); + var json = JsonSerializer.Deserialize(text); + Assert.True(json.GetProperty("sectionCount").GetInt32() > 0); + Assert.Contains(json.GetProperty("sections").EnumerateArray(), static section => + section.GetProperty("name").GetString() == "code"); + } + + /// + /// list_wasm_functions returns imported and file-backed functions in Wasm function-index order. + /// + [Fact(Timeout = 30_000)] + public async Task ListWasmFunctions_Direct_ReturnsFunctionInventory() + { + var wasmPath = GetWasmNativePath(); + + await StartServerAsync(); + await using var client = await CreateClientAsync(); + + var result = await client.CallToolAsync( + "list_wasm_functions", + new Dictionary { ["assemblyPath"] = wasmPath }, + cancellationToken: TestCancellationToken); + + var text = GetTextContent(result); + Assert.NotNull(text); + var json = JsonSerializer.Deserialize(text); + Assert.True(json.GetProperty("functionCount").GetInt32() > 0); + var functions = json.GetProperty("functions").EnumerateArray().ToArray(); + Assert.Contains(functions, static function => function.GetProperty("isImported").GetBoolean()); + Assert.Contains(functions, static function => !function.GetProperty("isImported").GetBoolean()); + } + + /// + /// list_wasm_functions forwards session requests to a running dotsider instance unchanged. + /// + [Fact(Timeout = 30_000)] + public async Task ListWasmFunctions_Session_ForwardsRequest() + { + await using var socket = new TestDotsiderSocket(999_997, "dotnet.native.wasm"); + socket.OnMethod("list-wasm-functions", _ => DotsiderResponse.Ok(new + { + FunctionCount = 1, + Functions = new[] { new { Index = 0, Name = "func_0" } } + })); + + await StartServerAsync(); + await using var client = await CreateClientAsync(); + + var result = await client.CallToolAsync( + "list_wasm_functions", + new Dictionary { ["sessionId"] = socket.Pid }, + cancellationToken: TestCancellationToken); + + var text = GetTextContent(result); + Assert.NotNull(text); + var json = JsonSerializer.Deserialize(text); + Assert.Equal(1, json.GetProperty("functionCount").GetInt32()); + Assert.Equal("func_0", json.GetProperty("functions")[0].GetProperty("name").GetString()); + } + + private string GetWasmNativePath() + { + Assert.SkipWhen(samples.WasmConsoleNativeWasm is null && samples.ReadyToRunConsoleWasmNativeWasm is null, + "browser-wasm publish did not run on this leg."); + + return samples.WasmConsoleNativeWasm ?? samples.ReadyToRunConsoleWasmNativeWasm!; + } +} diff --git a/tests/Dotsider.Tests/CliTests.cs b/tests/Dotsider.Tests/CliTests.cs index 8f881d51..23f32d53 100644 --- a/tests/Dotsider.Tests/CliTests.cs +++ b/tests/Dotsider.Tests/CliTests.cs @@ -589,8 +589,67 @@ public async Task Analyze_Symbols_Managed_ExitsOne() Assert.Contains("managed", stderr); } + /// + /// Verifies default and JSON analyze output report a raw SDK WebAssembly module as Wasm. + /// + [Fact(Timeout = 30_000)] + public async Task Analyze_Wasm_PrintsModuleSummary() + { + var wasmPath = GetWasmNativePath(); + + var (exitCode, stdout, _) = await RunDotsiderAsync( + "analyze", wasmPath); + + Assert.Equal(0, exitCode); + Assert.Contains("Kind: WebAssembly (.NET)", stdout); + Assert.Contains("Functions:", stdout); + Assert.Contains("Symbols:", stdout); + + var (jsonExitCode, jsonStdout, _) = await RunDotsiderAsync( + "analyze", wasmPath, "--json"); + + Assert.Equal(0, jsonExitCode); + var json = System.Text.Json.JsonSerializer.Deserialize(jsonStdout); + Assert.Equal("wasm", json.GetProperty("binaryKind").GetString()); + Assert.Equal("Wasm32", json.GetProperty("architecture").GetString()); + Assert.True(json.GetProperty("wasm").GetProperty("definedFunctionCount").GetInt32() > 0); + } + + /// + /// Verifies analyze --symbols on a raw Wasm module prints WebAssembly provenance. + /// + [Fact(Timeout = 30_000)] + public async Task Analyze_Symbols_Wasm_PrintsTable() + { + var wasmPath = GetWasmNativePath(); + + var (exitCode, stdout, _) = await RunDotsiderAsync( + "analyze", wasmPath, "--symbols"); + + Assert.Equal(0, exitCode); + Assert.Contains("WebAssembly", stdout); + Assert.Contains("Symbols (", stdout); + Assert.Contains("0x", stdout); + } + + /// + /// Verifies analyze --size on a raw Wasm module reports the Wasm function tree. + /// + [Fact(Timeout = 30_000)] + public async Task Analyze_Size_Wasm_PrintsFunctionBreakdown() + { + var wasmPath = GetWasmNativePath(); + + var (exitCode, stdout, _) = await RunDotsiderAsync( + "analyze", wasmPath, "--size"); + + Assert.Equal(0, exitCode); + Assert.Contains("(Wasm)", stdout); + Assert.Contains("Functions", stdout); + } + /// Verifies analyze --disasm 0xVA on a Native AOT binary prints a named listing. - [Fact(Timeout = 60_000)] + [Fact(Timeout = 30_000)] public async Task Analyze_Disasm_NativeAot_ByAddress_PrintsListing() { Assert.SkipWhen(fixture.NativeAotConsoleExe is null || !File.Exists(fixture.NativeAotConsoleExe), @@ -613,6 +672,77 @@ public async Task Analyze_Disasm_NativeAot_ByAddress_PrintsListing() Assert.Contains($"0x{va:x}:", stdout); } + /// + /// Verifies analyze --disasm on a raw Wasm module decodes a real function body. + /// + [Fact(Timeout = 30_000)] + public async Task Analyze_Disasm_Wasm_ByAddress_PrintsListing() + { + var wasmPath = GetWasmNativePath(); + + ulong va; + using (var analyzer = new Dotsider.Core.Analysis.AssemblyAnalyzer(wasmPath)) + { + var symbol = FindWasmFunctionWithNamedCall(analyzer); + va = symbol.VirtualAddress; + } + + var (exitCode, stdout, _) = await RunDotsiderAsync( + "analyze", wasmPath, "--disasm", $"0x{va:x}"); + + Assert.Equal(0, exitCode); + Assert.Contains($"0x{va:x}:", stdout); + Assert.Contains("call", stdout); + } + + /// + /// Verifies analyze --disasm accepts WebAssembly function identifiers in the forms users + /// see in wasm tooling: func:N and a bare decimal function index. + /// + [Fact(Timeout = 30_000)] + public async Task Analyze_Disasm_Wasm_ByFunctionIndex_PrintsListing() + { + var wasmPath = GetWasmNativePath(); + + string funcAlias; + string funcIndex; + using (var analyzer = new Dotsider.Core.Analysis.AssemblyAnalyzer(wasmPath)) + { + var symbol = analyzer.NativeSymbols!.Symbols.First(s => + s.Aliases.Any(static alias => alias.StartsWith("func:", StringComparison.Ordinal))); + funcAlias = symbol.Aliases.First(static alias => alias.StartsWith("func:", StringComparison.Ordinal)); + funcIndex = funcAlias["func:".Length..]; + } + + var (aliasExitCode, aliasStdout, _) = await RunDotsiderAsync( + "analyze", wasmPath, "--disasm", funcAlias); + var (indexExitCode, indexStdout, _) = await RunDotsiderAsync( + "analyze", wasmPath, "--disasm", funcIndex); + + Assert.Equal(0, aliasExitCode); + Assert.Equal(0, indexExitCode); + Assert.Contains("func[", aliasStdout); + Assert.Contains("func[", indexStdout); + } + + /// + /// Verifies Webcil-wrapped .wasm app assemblies use managed metadata CLI behavior. + /// + [Fact(Timeout = 30_000)] + public async Task Analyze_WebcilWasm_PrintsManagedMetadata() + { + Assert.SkipWhen(fixture.WasmConsoleWebcilWasm is null, + "browser-wasm publish did not produce the Webcil app assembly on this leg."); + + var (exitCode, stdout, _) = await RunDotsiderAsync( + "analyze", fixture.WasmConsoleWebcilWasm!); + + Assert.Equal(0, exitCode); + Assert.Contains("Kind: Managed", stdout); + Assert.Contains("Webcil:", stdout); + Assert.DoesNotContain("Kind: WebAssembly (.NET)", stdout); + } + /// Verifies analyze --disasm on a managed assembly exits 1 with a native-symbols error. [Fact(Timeout = 30_000)] public async Task Analyze_Disasm_Managed_ExitsOne() @@ -839,4 +969,30 @@ public async Task Analyze_Correlate_ManagedAssembly_ExitsOne() private static Task<(int ExitCode, string Stdout, string Stderr)> RunDotsiderAsync( params string[] arguments) => TestHelpers.RunDotsiderAsync(arguments); + + private static Dotsider.Core.Analysis.Models.NativeSymbol FindWasmFunctionWithNamedCall( + Dotsider.Core.Analysis.AssemblyAnalyzer analyzer) + { + var info = analyzer.NativeSymbols; + Assert.NotNull(info); + foreach (var symbol in info.Symbols.Take(512)) + { + var result = Dotsider.Core.Analysis.Disasm.NativeDisassembler.DisassembleSymbol(analyzer, symbol); + if (result is null) + continue; + + if (result.Value.Instructions.Any(static instruction => instruction.TargetName is not null)) + return symbol; + } + + throw new InvalidOperationException("No Wasm function with a named direct call was found."); + } + + private string GetWasmNativePath() + { + Assert.SkipWhen(fixture.WasmConsoleNativeWasm is null && fixture.ReadyToRunConsoleWasmNativeWasm is null, + "browser-wasm publish did not run on this leg."); + + return fixture.WasmConsoleNativeWasm ?? fixture.ReadyToRunConsoleWasmNativeWasm!; + } } diff --git a/tests/Dotsider.Tests/DotsiderStateTests.cs b/tests/Dotsider.Tests/DotsiderStateTests.cs index 6b2b54d0..ab322d15 100644 --- a/tests/Dotsider.Tests/DotsiderStateTests.cs +++ b/tests/Dotsider.Tests/DotsiderStateTests.cs @@ -549,6 +549,28 @@ public void NavigateToHexOffset_SetsCursorPosition() Assert.Equal(expectedOffset, state.HexScrollTarget); } + /// + /// Verifies a raw Wasm function can jump to its file-backed bytes in the Hex Dump. + /// + [Fact(Timeout = 30_000)] + public void NavigateToHexFileOffset_WasmFunction_SetsCursorPosition() + { + var wasmPath = GetWasmNativePath(); + var app = CreateApp(); + using var state = new DotsiderState(app, wasmPath); + state.CurrentTab = TabId.IlInspector; + + var symbol = state.Analyzer.NativeSymbols!.Symbols.First(s => s.FileOffset is not null && s.Size > 0); + state.IlSelectedNativeSymbol = symbol; + state.NavigateToHexFileOffset(symbol.FileOffset!.Value); + + Assert.Equal(TabId.HexDump, state.CurrentTab); + Assert.Equal((int)symbol.FileOffset.Value, state.HexEditorState.ByteCursorOffset); + Assert.Equal(symbol.FileOffset.Value, state.HexScrollTarget); + Assert.NotNull(state.CrossViewBackTarget); + Assert.Equal(TabId.IlInspector, state.CrossViewBackTarget!.Value.Tab); + } + /// /// Verifies navigate to hex offset invalid rva no tab switch. /// @@ -788,4 +810,12 @@ public void Dispose() _workload?.Dispose(); GC.SuppressFinalize(this); } + + private string GetWasmNativePath() + { + Assert.SkipWhen(samples.WasmConsoleNativeWasm is null && samples.ReadyToRunConsoleWasmNativeWasm is null, + "browser-wasm sample publish did not produce dotnet.native.wasm on this leg."); + + return samples.WasmConsoleNativeWasm ?? samples.ReadyToRunConsoleWasmNativeWasm!; + } } diff --git a/tests/Dotsider.Tests/PeMetadataViewTests.cs b/tests/Dotsider.Tests/PeMetadataViewTests.cs index e120bc69..27e4c292 100644 --- a/tests/Dotsider.Tests/PeMetadataViewTests.cs +++ b/tests/Dotsider.Tests/PeMetadataViewTests.cs @@ -1,3 +1,4 @@ +using Dotsider.Core.Analysis.Models; using Hex1b; using Hex1b.Automation; using Hex1b.Input; @@ -1084,6 +1085,66 @@ await builder await runTask; } + /// + /// Verifies raw WebAssembly modules reuse the metadata sub-tabs with Wasm-specific tables. + /// + [Fact(Timeout = 30_000)] + public async Task PeMetadata_Wasm_SubTabsRenderWasmTables() + { + var wasmPath = GetWasmNativePath(); + + var (terminal, app) = CreateDotsiderApp(wasmPath); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken); + var runTask = app.RunAsync(cts.Token); + await Task.Delay(100, cts.Token); + + var builder = new Hex1bTerminalInputSequenceBuilder() + .WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10)) + .WaitUntil(s => s.ContainsText("Sections"), TimeSpan.FromSeconds(10)) + .WaitUntil(s => s.ContainsText("Payload Offset"), TimeSpan.FromSeconds(10)); + + builder = ExpectNextWasmSubTab(builder, PeSubTabId.TypeDef, "Types", "Params"); + builder = ExpectNextWasmSubTab(builder, PeSubTabId.MethodDef, "Functions", "Kind"); + builder = ExpectNextWasmSubTab(builder, PeSubTabId.TypeRef, "Tables", "Ref Type"); + builder = ExpectNextWasmSubTab(builder, PeSubTabId.MemberRef, "Memories", "Min Pages"); + builder = ExpectNextWasmSubTab(builder, PeSubTabId.Attributes, "Globals", "Mutable"); + builder = ExpectNextWasmSubTab(builder, PeSubTabId.Resources, "Data", "Mode"); + builder = ExpectNextWasmSubTab(builder, PeSubTabId.DebugDirectory, "Custom", "Offset"); + builder = ExpectNextWasmSubTab(builder, PeSubTabId.Imports, "Imports", "Module"); + builder = ExpectNextWasmSubTab(builder, PeSubTabId.Exports, "Exports", "Name"); + builder = ExpectNextWasmSubTab(builder, PeSubTabId.LoadConfig, "Elements", "Element"); + builder = ExpectNextWasmSubTab(builder, PeSubTabId.RtrSections, "Tags", "Attribute"); + builder = ExpectNextWasmSubTab(builder, PeSubTabId.AotTypes, "Module", "Version"); + builder = ExpectNextWasmSubTab(builder, PeSubTabId.Symbols, "Symbols", "Address"); + + await builder + .WaitUntil(s => !s.ContainsText("TypeDef") && !s.ContainsText("MethodDef"), TimeSpan.FromSeconds(10)) + .Build() + .ApplyAsync(terminal, cts.Token); + + Assert.Equal(PeSubTabId.Symbols, _state!.PeSubTab); + Assert.Equal(BinaryKind.Wasm, _state.Analyzer.BinaryKind); + + cts.Cancel(); + await runTask; + } + + private Hex1bTerminalInputSequenceBuilder ExpectNextWasmSubTab( + Hex1bTerminalInputSequenceBuilder builder, int expectedSubTab, string label, string tableText) => + builder + .Key(Hex1bKey.RightArrow) + .WaitUntil(_ => _state!.PeSubTab == expectedSubTab, TimeSpan.FromSeconds(10)) + .WaitUntil(s => s.ContainsText(label), TimeSpan.FromSeconds(10)) + .WaitUntil(s => s.ContainsText(tableText), TimeSpan.FromSeconds(10)); + + private string GetWasmNativePath() + { + Assert.SkipWhen(samples.WasmConsoleNativeWasm is null && samples.ReadyToRunConsoleWasmNativeWasm is null, + "browser-wasm sample publish did not produce dotnet.native.wasm on this leg."); + + return samples.WasmConsoleNativeWasm ?? samples.ReadyToRunConsoleWasmNativeWasm!; + } + /// /// Disposes test resources created during the run. /// diff --git a/tests/Dotsider.Tests/SampleAssemblyFixture.cs b/tests/Dotsider.Tests/SampleAssemblyFixture.cs index 4619331c..cbf64fc7 100644 --- a/tests/Dotsider.Tests/SampleAssemblyFixture.cs +++ b/tests/Dotsider.Tests/SampleAssemblyFixture.cs @@ -272,6 +272,27 @@ public class SampleAssemblyFixture : IAsyncLifetime /// public string? ReadyToRunConsoleWasmNativeWasm { get; private set; } + /// + /// Path to the dedicated SDK-produced browser Wasm runtime module from samples/WasmConsole. + /// Raw Wasm open, symbols, disassembly, and size tests prefer this focused fixture. + /// means the current SDK/workload set cannot publish browser Wasm. + /// + public string? WasmConsoleNativeWasm { get; private set; } + + /// + /// Path to the AOT-compiled browser Wasm runtime module from samples/WasmConsole. + /// This fixture is produced with RunAOTCompilation=true when the SDK supports it. + /// means the current SDK/workload set cannot publish browser Wasm AOT. + /// + public string? WasmConsoleAotNativeWasm { get; private set; } + + /// + /// Path to the Webcil-wrapped managed app assembly from samples/WasmConsole. + /// Managed Webcil tests use this to verify .wasm opens as metadata/IL, not native Wasm. + /// means the current SDK/workload set cannot publish browser Wasm. + /// + public string? WasmConsoleWebcilWasm { get; private set; } + /// /// Path to the composite global image (ReadyToRunComposite.r2r.dll) — metadata-less native /// PE whose components resolve from siblings — or null when the composite publish did not run. @@ -352,9 +373,11 @@ public async ValueTask InitializeAsync() builds.Add(PublishReadyToRunProject("samples/ReadyToRunConsole", "linux-riscv64", selfContained: true)); builds.Add(PublishReadyToRunProject("samples/ReadyToRunConsole", "linux-loongarch64", selfContained: true)); builds.Add(PublishWasmProject("samples/ReadyToRunConsole")); + builds.Add(PublishWasmProject("samples/WasmConsole")); builds.Add(PublishReadyToRunProject("samples/ReadyToRunComposite", selfContained: true)); await Task.WhenAll(builds); + await PublishWasmProject("samples/WasmConsole", runAotCompilation: true); var config = "Debug"; var tfm = "net10.0"; @@ -481,6 +504,12 @@ public async ValueTask InitializeAsync() "bin", "Release", tfm, "linux-loongarch64", "publish", "ReadyToRunConsole.dll")); ReadyToRunConsoleWasmNativeWasm = ExistingPathOrNull(Path.Combine(_repoRoot, "samples", "ReadyToRunConsole", "bin", "Release", tfm, "browser-wasm", "publish", "dotnet.native.wasm")); + WasmConsoleNativeWasm = ExistingPathOrNull(Path.Combine(_repoRoot, "samples", "WasmConsole", + "bin", "Release", tfm, "browser-wasm", "publish", "dotnet.native.wasm")); + WasmConsoleAotNativeWasm = ExistingPathOrNull(Path.Combine(_repoRoot, "samples", "WasmConsole", + "bin", "Release", tfm, "browser-wasm-aot", "publish", "dotnet.native.wasm")); + WasmConsoleWebcilWasm = ExistingPathOrNull(Path.Combine(_repoRoot, "samples", "WasmConsole", + "bin", "Release", tfm, "browser-wasm", "AppBundle", "_framework", "WasmConsole.wasm")); var r2rCompositeDir = Path.Combine(_repoRoot, "samples", "ReadyToRunComposite", "bin", "Release", tfm, rid, "publish"); @@ -712,9 +741,15 @@ private async Task PublishReadyToRunProject(string relativePath, string rid, boo /// The produced dotnet.native.wasm module still contains real runtime Wasm code. /// A missing wasm-tools workload leaves the outputs absent so dependent tests can skip. /// - private async Task PublishWasmProject(string relativePath) + private async Task PublishWasmProject(string relativePath, bool runAotCompilation = false) { - var lockName = "dotsider-build-" + relativePath.Replace('/', '-').Replace('\\', '-') + "-browser-wasm.lock"; + var wasmRid = runAotCompilation ? "browser-wasm-aot" : "browser-wasm"; + var expectedOutput = Path.Combine(_repoRoot, relativePath, + "bin", "Release", "net10.0", wasmRid, "publish", "dotnet.native.wasm"); + if (File.Exists(expectedOutput)) + return; + + var lockName = "dotsider-build-" + relativePath.Replace('/', '-').Replace('\\', '-') + $"-{wasmRid}.lock"; var lockPath = Path.Combine(Path.GetTempPath(), lockName); FileStream lockFile; @@ -734,11 +769,19 @@ private async Task PublishWasmProject(string relativePath) try { var projectDir = Path.Combine(_repoRoot, relativePath); + var arguments = "publish -c Release -r browser-wasm --self-contained true " + + "-p:PublishReadyToRun=false -p:WasmEmitSymbolMap=true "; + if (runAotCompilation) + { + arguments += "-p:RunAOTCompilation=true " + + "-p:WasmAppDir=bin\\Release\\net10.0\\browser-wasm-aot\\AppBundle " + + "-p:PublishDir=bin\\Release\\net10.0\\browser-wasm-aot\\publish\\ "; + } + var psi = new ProcessStartInfo { FileName = "dotnet", - Arguments = "publish -c Release -r browser-wasm --self-contained true " - + "-p:PublishReadyToRun=false -v q", + Arguments = arguments + "-v q", WorkingDirectory = projectDir, UseShellExecute = false, RedirectStandardOutput = true, diff --git a/tests/Dotsider.Tests/SessionBundleTests.cs b/tests/Dotsider.Tests/SessionBundleTests.cs index 9f9c3069..56a49c30 100644 --- a/tests/Dotsider.Tests/SessionBundleTests.cs +++ b/tests/Dotsider.Tests/SessionBundleTests.cs @@ -534,6 +534,32 @@ await TestHelpers.WaitUntilAsync( Assert.True(_state!.NavigationStack.Count > depthBefore); } + /// + /// Verifies that push-assembly by path accepts a raw SDK-produced WebAssembly runtime module. + /// + [Fact(Timeout = 30_000)] + public async Task PushAssembly_ByWasmPath_OpensRawWasmModule() + { + var wasmPath = GetWasmNativePath(); + var ct = TestContext.Current.CancellationToken; + var (_, socketPath) = await StartTuiWithDiagnosticsAsync(samples.RichLibraryDll, ct); + + var pushResponse = await DotsiderClient.SendAsync(socketPath, + new DotsiderRequest { Method = "push-assembly", AssemblyPath = wasmPath }, ct); + Assert.True(pushResponse.Success); + await TestHelpers.WaitUntilAsync( + () => _state?.Analyzer.BinaryKind == global::Dotsider.Core.Analysis.Models.BinaryKind.Wasm, + TimeSpan.FromSeconds(5)); + + var info = await DotsiderClient.SendAsync(socketPath, + new DotsiderRequest { Method = "assembly-info" }, ct); + Assert.True(info.Success); + var data = (info.Data as JsonElement?)!.Value; + Assert.Equal("wasm", data.GetProperty("binaryKind").GetString()); + Assert.False(data.GetProperty("hasMetadata").GetBoolean()); + Assert.True(data.GetProperty("wasm").GetProperty("definedFunctionCount").GetInt32() > 0); + } + /// /// Verifies that push-assembly by path correctly handles an apphost exe /// by loading the companion managed assembly instead of the native host. @@ -621,6 +647,15 @@ await TestHelpers.WaitUntilAsync( Assert.NotNull(_state.ApphostCompanionDllPath); } + private string GetWasmNativePath() + { + Assert.SkipWhen( + samples.WasmConsoleNativeWasm is null && samples.ReadyToRunConsoleWasmNativeWasm is null, + "browser-wasm publish did not run on this leg."); + + return samples.WasmConsoleNativeWasm ?? samples.ReadyToRunConsoleWasmNativeWasm!; + } + /// /// Disposes the diagnostics listener, state, and terminal. /// diff --git a/tests/Dotsider.Tests/SessionSymbolTests.cs b/tests/Dotsider.Tests/SessionSymbolTests.cs index dc6304e7..ea00d2e7 100644 --- a/tests/Dotsider.Tests/SessionSymbolTests.cs +++ b/tests/Dotsider.Tests/SessionSymbolTests.cs @@ -101,6 +101,56 @@ public async Task GetNativeSymbols_Managed_Fails() Assert.Contains("no native symbols", response.Error); } + /// + /// Verifies get-native-symbols returns WebAssembly function symbols from a raw SDK + /// browser-wasm runtime module opened in the TUI. + /// + [Fact(Timeout = 30_000)] + public async Task GetNativeSymbols_Wasm_ReturnsSymbolsWithProvenance() + { + var wasmPath = GetWasmNativePath(); + + var ct = TestContext.Current.CancellationToken; + var socketPath = await StartTuiWithDiagnosticsAsync(wasmPath, ct); + + var response = await DotsiderClient.SendAsync(socketPath, + new DotsiderRequest { Method = "get-native-symbols" }, ct); + + Assert.True(response.Success); + var data = (response.Data as JsonElement?)!.Value; + Assert.Equal("webAssembly", data.GetProperty("source").GetString()); + Assert.Equal("wasm32", data.GetProperty("architecture").GetString()); + Assert.True(data.GetProperty("symbols").GetArrayLength() > 0); + } + + /// + /// Verifies disassemble-native accepts WebAssembly func:N identifiers through + /// the diagnostics socket, matching the CLI and MCP native-disassembly surfaces. + /// + [Fact(Timeout = 30_000)] + public async Task DisassembleNative_Wasm_ByFunctionIndex_ReturnsInstructions() + { + var wasmPath = GetWasmNativePath(); + string funcAlias; + using (var analyzer = new Dotsider.Core.Analysis.AssemblyAnalyzer(wasmPath)) + { + var symbol = analyzer.NativeSymbols!.Symbols.First(s => + s.Aliases.Any(static alias => alias.StartsWith("func:", StringComparison.Ordinal))); + funcAlias = symbol.Aliases.First(static alias => alias.StartsWith("func:", StringComparison.Ordinal)); + } + + var ct = TestContext.Current.CancellationToken; + var socketPath = await StartTuiWithDiagnosticsAsync(wasmPath, ct); + + var response = await DotsiderClient.SendAsync(socketPath, + new DotsiderRequest { Method = "disassemble-native", SymbolName = funcAlias }, ct); + + Assert.True(response.Success); + var data = (response.Data as JsonElement?)!.Value; + Assert.Equal("Wasm32", data.GetProperty("architecture").GetString()); + Assert.True(data.GetProperty("instructions").GetArrayLength() > 0); + } + /// /// Verifies assembly-info carries the native symbol count, source, and status for a /// Native AOT binary. @@ -123,6 +173,14 @@ public async Task AssemblyInfo_NativeAot_CarriesSymbolProvenance() Assert.False(string.IsNullOrEmpty(data.GetProperty("nativeSymbolStatus").GetString())); } + private string GetWasmNativePath() + { + Assert.SkipWhen(samples.WasmConsoleNativeWasm is null && samples.ReadyToRunConsoleWasmNativeWasm is null, + "browser-wasm publish did not run on this leg."); + + return samples.WasmConsoleNativeWasm ?? samples.ReadyToRunConsoleWasmNativeWasm!; + } + /// /// Disposes the diagnostics listener, state, and terminal. /// diff --git a/tests/Dotsider.Tests/StandardModeViewTests.cs b/tests/Dotsider.Tests/StandardModeViewTests.cs index 9fdab1ff..f075b089 100644 --- a/tests/Dotsider.Tests/StandardModeViewTests.cs +++ b/tests/Dotsider.Tests/StandardModeViewTests.cs @@ -21,6 +21,14 @@ public class StandardModeViewTests(SampleAssemblyFixture samples) : IDisposable private (Hex1bTerminal terminal, Hex1bApp app) CreateDotsiderApp(string dllPath, int? initialTab = null) => CreateDotsiderAppCore(dllPath, initialTab, enableMouse: false, enableInputCoalescing: false); + private (Hex1bTerminal terminal, Hex1bApp app) CreateDotsiderAppWithDimensions( + string dllPath, + int width, + int height, + int? initialTab = null) + => CreateDotsiderAppCore(dllPath, initialTab, enableMouse: false, + enableInputCoalescing: false, width, height); + /// /// Variant of that turns on mouse support, matching the /// production EnableMouse = true knob set at Program.cs:209/319/376. Used @@ -45,13 +53,18 @@ public class StandardModeViewTests(SampleAssemblyFixture samples) : IDisposable enableInputCoalescing: enableInputCoalescing); private (Hex1bTerminal terminal, Hex1bApp app) CreateDotsiderAppCore( - string dllPath, int? initialTab, bool enableMouse, bool enableInputCoalescing) + string dllPath, + int? initialTab, + bool enableMouse, + bool enableInputCoalescing, + int width = 120, + int height = 30) { _workload = new Hex1bAppWorkloadAdapter(); _terminal = Hex1bTerminal.CreateBuilder() .WithWorkload(_workload) .WithHeadless() - .WithDimensions(120, 30) + .WithDimensions(width, height) .Build(); DotsiderApp? dotsiderApp = null; _hex1bApp = new Hex1bApp( @@ -217,6 +230,103 @@ public async Task Tab3_Label_NativeAot_IsDisassembly() await runTask; } + /// + /// Verifies tab3 is labeled as disassembly for a raw SDK browser-wasm runtime module. + /// + [Fact(Timeout = 30_000)] + public async Task Tab3_Label_Wasm_IsDisassembly() + { + var wasmPath = GetWasmNativePath(); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken); + var (terminal, app) = CreateDotsiderApp(wasmPath); + var runTask = app.RunAsync(cts.Token); + await Task.Delay(100, cts.Token); + + await new Hex1bTerminalInputSequenceBuilder() + .WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10)) + .WaitUntil(s => s.ContainsText("Disassembly"), TimeSpan.FromSeconds(10)) + .Build() + .ApplyAsync(terminal, cts.Token); + + cts.Cancel(); + await runTask; + } + + /// + /// Verifies the Wasm General tab keeps the references section visible in a + /// short terminal even though the WebAssembly summary is taller than the pane. + /// + [Fact(Timeout = 30_000)] + public async Task General_Wasm_ShortTerminal_KeepsAssemblyReferencesVisible() + { + var wasmPath = GetWasmNativePath(); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken); + var (terminal, app) = CreateDotsiderAppWithDimensions(wasmPath, width: 110, height: 20); + var runTask = app.RunAsync(cts.Token); + await Task.Delay(100, cts.Token); + + await new Hex1bTerminalInputSequenceBuilder() + .WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10)) + .WaitUntil(s => s.ContainsText("dotnet.native.wasm"), TimeSpan.FromSeconds(10)) + .WaitUntil(s => s.ContainsText("Wasm32"), TimeSpan.FromSeconds(10)) + .WaitUntil(s => s.ContainsText("Assembly References"), TimeSpan.FromSeconds(10)) + .Build() + .ApplyAsync(terminal, cts.Token); + + cts.Cancel(); + await runTask; + } + + /// + /// Verifies the PE/Metadata tab routes raw Wasm modules to WebAssembly section rows. + /// + [Fact(Timeout = 30_000)] + public async Task Tab1_Wasm_ShowsWebAssemblySections() + { + var wasmPath = GetWasmNativePath(); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken); + var (terminal, app) = CreateDotsiderApp(wasmPath, TabId.PeMetadata); + var runTask = app.RunAsync(cts.Token); + await Task.Delay(100, cts.Token); + + await new Hex1bTerminalInputSequenceBuilder() + .WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10)) + .WaitUntil(s => s.ContainsText("Payload Offset"), TimeSpan.FromSeconds(10)) + .WaitUntil(s => s.ContainsText("code") || s.ContainsText("type"), TimeSpan.FromSeconds(10)) + .Build() + .ApplyAsync(terminal, cts.Token); + + cts.Cancel(); + await runTask; + } + + /// + /// Verifies the Disassembly tab presents raw Wasm functions through Wasm-specific groups. + /// + [Fact(Timeout = 30_000)] + public async Task Tab3_Wasm_ShowsWasmFunctionGroups() + { + var wasmPath = GetWasmNativePath(); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken); + var (terminal, app) = CreateDotsiderApp(wasmPath, TabId.IlInspector); + var runTask = app.RunAsync(cts.Token); + await Task.Delay(100, cts.Token); + + await new Hex1bTerminalInputSequenceBuilder() + .WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10)) + .WaitUntil(s => s.ContainsText("(imports)"), TimeSpan.FromSeconds(10)) + .WaitUntil(s => s.ContainsText("(exports)") || s.ContainsText("(functions)"), TimeSpan.FromSeconds(10)) + .Build() + .ApplyAsync(terminal, cts.Token); + + cts.Cancel(); + await runTask; + } + /// /// Verifies tab3 is labeled as IL plus native for ReadyToRun images. /// @@ -3199,6 +3309,14 @@ public async Task General_NativeAot_ShowsAotInfo() await runTask; } + private string GetWasmNativePath() + { + Assert.SkipWhen(samples.WasmConsoleNativeWasm is null && samples.ReadyToRunConsoleWasmNativeWasm is null, + "browser-wasm publish did not run on this leg."); + + return samples.WasmConsoleNativeWasm ?? samples.ReadyToRunConsoleWasmNativeWasm!; + } + /// /// Disposes test resources created during the run. /// diff --git a/tests/Dotsider.Tests/TestHelpers.cs b/tests/Dotsider.Tests/TestHelpers.cs index 29c15a76..ffdd1012 100644 --- a/tests/Dotsider.Tests/TestHelpers.cs +++ b/tests/Dotsider.Tests/TestHelpers.cs @@ -59,8 +59,6 @@ internal static string GetRepoRoot() throw new InvalidOperationException("Could not find repo root (Dotsider.slnx)"); } - private static readonly string s_dotsiderProjectPath = Path.Combine(GetRepoRoot(), "src", "Dotsider"); - private static readonly string s_dotsiderBuildConfig = DetectBuildConfig(); private static string DetectBuildConfig() @@ -82,11 +80,15 @@ private static string DetectBuildConfig() internal static async Task<(int ExitCode, string Stdout, string Stderr)> RunDotsiderAsync( params string[] arguments) { + var copiedCli = Path.Combine(AppContext.BaseDirectory, "dotsider.dll"); + var runArgs = File.Exists(copiedCli) + ? $"\"{copiedCli}\" " + : $"run --no-build -c {s_dotsiderBuildConfig} --project \"{Path.Combine(GetRepoRoot(), "src", "Dotsider")}\" -- "; + var psi = new ProcessStartInfo { FileName = "dotnet", - Arguments = $"run --no-build -c {s_dotsiderBuildConfig} --project \"{s_dotsiderProjectPath}\" -- " - + string.Join(' ', arguments.Select(QuoteArg)), + Arguments = runArgs + string.Join(' ', arguments.Select(QuoteArg)), UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, diff --git a/tests/Dotsider.Tests/WasmSdkModuleDecoderTests.cs b/tests/Dotsider.Tests/WasmSdkModuleDecoderTests.cs index 726347df..7c00ffca 100644 --- a/tests/Dotsider.Tests/WasmSdkModuleDecoderTests.cs +++ b/tests/Dotsider.Tests/WasmSdkModuleDecoderTests.cs @@ -1,143 +1,275 @@ +using Dotsider.Core.Analysis; using Dotsider.Core.Analysis.Disasm; using Dotsider.Core.Analysis.Models; namespace Dotsider.Tests; /// -/// Verifies the Wasm decoder against a real SDK-produced browser Wasm module. -/// The fixture publish emits dotnet.native.wasm with the Mono runtime and app code. -/// This covers real Wasm function bodies without pretending they are ReadyToRun images. +/// Verifies raw WebAssembly support against a real SDK-produced browser-wasm module. +/// The fixture publish emits dotnet.native.wasm and dotnet.native.js.symbols. +/// These tests cover the public analyzer path users exercise when they open the module directly. /// [Collection("SampleAssemblies")] public sealed class WasmSdkModuleDecoderTests(SampleAssemblyFixture samples) { /// - /// Parses the code section from dotnet.native.wasm and decodes real function bodies. - /// The test is skipped when the host SDK lacks the browser-wasm workload. - /// Fallback instructions indicate that emitted Wasm bytecode was not modeled. + /// Opening dotnet.native.wasm classifies the file as WebAssembly, reports Wasm32, + /// and preserves the module's section, import, export, function, and symbol-map facts. /// [Fact(Timeout = 30_000)] - public void BrowserWasmNativeModule_FunctionBodiesDecodeWithoutFallback() + public void BrowserWasmNativeModule_OpensAsWasmModule() { - Assert.SkipWhen( - samples.ReadyToRunConsoleWasmNativeWasm is null, - "browser-wasm publish did not run on this leg."); + using var analyzer = OpenWasmFixture(); - var module = File.ReadAllBytes(samples.ReadyToRunConsoleWasmNativeWasm!); - var bodies = ReadFunctionBodies(module) - .Where(static body => body.Code.Length > 0) - .Take(32) - .ToList(); + Assert.False(analyzer.HasMetadata); + Assert.Equal(BinaryKind.Wasm, analyzer.BinaryKind); + Assert.Equal("Wasm32", analyzer.Architecture); - Assert.True(bodies.Count >= 16, "Expected at least 16 non-empty Wasm function bodies."); + var wasm = analyzer.WasmModuleInfo; + Assert.NotNull(wasm); + Assert.Equal(1, wasm.Version); + Assert.NotEmpty(wasm.Sections); + Assert.True(wasm.ImportedFunctionCount > 0); + Assert.True(wasm.DefinedFunctionCount > 0); + Assert.True(wasm.CodeSize > 0); + Assert.True(wasm.DataSize > 0); + Assert.Equal(WasmSymbolMapStatus.Loaded, wasm.SymbolMapStatus); + Assert.True(wasm.SymbolMapEntryCount > wasm.DefinedFunctionCount / 2); + } - var sawCall = false; - var sawLocalGet = false; - var failures = new List(); + /// + /// The raw Wasm reader parses standard SDK-emitted sections into model facts instead of only + /// preserving their raw section headers. + /// + [Fact(Timeout = 30_000)] + public void BrowserWasmNativeModule_ParsesStandardSections() + { + using var analyzer = OpenWasmFixture(); - foreach (var body in bodies) - { - var instructions = NativeDisassembler.Disassemble( - body.Code, (ulong)body.CodeOffset, NativeArchitecture.Wasm32); - var length = instructions.Sum(static instruction => instruction.Length); - var fallback = instructions.FirstOrDefault(static instruction => instruction.IsFallback); + var wasm = analyzer.WasmModuleInfo; + Assert.NotNull(wasm); + Assert.NotEmpty(wasm.Types); + Assert.All(wasm.Functions.Where(static f => f.TypeIndex is not null), f => + Assert.InRange(f.TypeIndex!.Value, 0, wasm.Types.Count - 1)); - sawCall |= instructions.Any(static instruction => instruction.Mnemonic == "call"); - sawLocalGet |= instructions.Any(static instruction => instruction.Mnemonic == "local.get"); + AssertParsedWhenSectionExists(wasm, 4, wasm.Tables.Count, "table"); + AssertParsedWhenSectionExists(wasm, 5, wasm.Memories.Count, "memory"); + AssertParsedWhenSectionExists(wasm, 6, wasm.Globals.Count, "global"); + AssertParsedWhenSectionExists(wasm, 9, wasm.Elements.Count, "element"); + AssertParsedWhenSectionExists(wasm, 13, wasm.Tags.Count, "tag"); + AssertDefinedIndexesStartAfterImports(wasm); - if (length != body.Code.Length || fallback is not null) - { - failures.Add( - $"body {body.Index} @ 0x{body.CodeOffset:x}: " - + $"length={length}/{body.Code.Length}, fallback={fallback?.Mnemonic} {fallback?.OperandText}"); - } - } + if (wasm.Sections.Any(static s => s.Id == 8)) + Assert.NotNull(wasm.StartFunctionIndex); + if (wasm.Sections.Any(static s => s.Id == 12)) + Assert.Equal(wasm.DataSegments.Count, wasm.DataCount); + } + + /// + /// A browser-wasm publish with RunAOTCompilation=true still opens through the raw + /// WebAssembly path and exposes real file-backed Wasm functions. + /// + [Fact(Timeout = 30_000)] + public void BrowserWasmAotNativeModule_OpensAsWasmModule() + { + Assert.SkipWhen(samples.WasmConsoleAotNativeWasm is null, + "browser-wasm AOT publish did not run on this leg."); - Assert.True(failures.Count == 0, string.Join(Environment.NewLine, failures)); - Assert.True(sawCall, "Expected at least one SDK-produced Wasm body to contain a call."); - Assert.True(sawLocalGet, "Expected at least one SDK-produced Wasm body to contain local.get."); + using var analyzer = new AssemblyAnalyzer(samples.WasmConsoleAotNativeWasm!); + + Assert.False(analyzer.HasMetadata); + Assert.Equal(BinaryKind.Wasm, analyzer.BinaryKind); + var wasm = analyzer.WasmModuleInfo; + Assert.NotNull(wasm); + Assert.True(wasm.DefinedFunctionCount > 0); + Assert.True(wasm.CodeSize > 0); + Assert.NotEmpty(analyzer.NativeSymbols?.Symbols ?? []); } - private static List ReadFunctionBodies(ReadOnlySpan module) + /// + /// Opening a Webcil-wrapped .wasm app assembly unwraps to managed metadata and IL + /// instead of presenting the wrapper as native runtime WebAssembly. + /// + [Fact(Timeout = 30_000)] + public void BrowserWasmWebcilAssembly_OpensAsManagedMetadata() { - if (module.Length < 8 - || module[0] != 0x00 || module[1] != 0x61 || module[2] != 0x73 || module[3] != 0x6D - || module[4] != 0x01 || module[5] != 0x00 || module[6] != 0x00 || module[7] != 0x00) - { - throw new InvalidDataException("The fixture is not a WebAssembly 1.0 module."); - } + using var analyzer = OpenWebcilFixture(); + + Assert.True(analyzer.HasMetadata); + Assert.Equal(BinaryKind.Managed, analyzer.BinaryKind); + Assert.NotNull(analyzer.WebcilInfo); + Assert.Null(analyzer.WasmModuleInfo); + Assert.Contains(analyzer.TypeDefs, static t => t.FullName == "WasmCalculator"); + + var method = analyzer.MethodDefs.First(static m => + m.DeclaringType == "WasmCalculator" && m.Name == "Add"); + var il = new IlDisassembler(analyzer).DisassembleWithText(method); + Assert.NotNull(il); + Assert.Contains("IL_", il.Value.Text); + Assert.Contains("ldarg", il.Value.Text); + } - var pos = 8; - while (pos < module.Length) + /// + /// WebAssembly functions become native symbols with WebAssembly provenance and file offsets. + /// The symbol names come from the SDK sidecar when dotnet.native.js.symbols is present. + /// + [Fact(Timeout = 30_000)] + public void BrowserWasmNativeModule_NativeSymbolsUseSymbolMap() + { + using var analyzer = OpenWasmFixture(); + + var info = analyzer.NativeSymbols; + Assert.NotNull(info); + Assert.Equal(NativeSymbolSource.WebAssembly, info.Source); + Assert.Equal(NativeSymbolStatus.Loaded, info.Status); + Assert.Equal(NativeArchitecture.Wasm32, info.Architecture); + Assert.NotNull(info.Path); + Assert.NotEmpty(info.Symbols); + Assert.All(info.Symbols.Take(100), symbol => { - var sectionId = ReadByte(module, ref pos); - var sectionSize = checked((int)ReadUleb(module, ref pos)); - var sectionEnd = checked(pos + sectionSize); - if (sectionEnd > module.Length) - throw new InvalidDataException("The WebAssembly section extends past the module."); + Assert.Equal(NativeSymbolKind.Function, symbol.Kind); + Assert.NotNull(symbol.FileOffset); + Assert.True(symbol.Size > 0); + }); + } - if (sectionId == 10) - return ReadCodeSection(module[pos..sectionEnd], pos); + /// + /// Disassembling a real SDK-produced Wasm function produces full instruction coverage and + /// resolves direct call operands to imported or defined function names. + /// + [Fact(Timeout = 30_000)] + public void BrowserWasmNativeModule_DisassemblesRealFunctionAndNamesDirectCalls() + { + using var analyzer = OpenWasmFixture(); + var symbol = FindFunctionWithNamedCall(analyzer); - pos = sectionEnd; - } + var result = NativeDisassembler.DisassembleSymbol(analyzer, symbol); + Assert.NotNull(result); - throw new InvalidDataException("The WebAssembly module does not contain a code section."); + var instructions = result.Value.Instructions; + Assert.NotEmpty(instructions); + Assert.DoesNotContain(instructions, static instruction => instruction.IsFallback); + Assert.Equal(symbol.Size, instructions.Sum(static instruction => instruction.Length)); + Assert.Contains(instructions, static instruction => + instruction.Mnemonic is "call" or "return_call" + && instruction.TargetName is not null); } - private static List ReadCodeSection(ReadOnlySpan section, int sectionOffset) + /// + /// Wasm disassembly annotates function-index, local, and table/type operands without treating + /// function indexes as native virtual addresses. + /// + [Fact(Timeout = 30_000)] + public void BrowserWasmNativeModule_DisassemblyAnnotatesWasmOperands() { - var bodies = new List(); - var pos = 0; - var count = checked((int)ReadUleb(section, ref pos)); + using var analyzer = OpenWasmFixture(); + var info = analyzer.NativeSymbols; + Assert.NotNull(info); - for (var index = 0; index < count; index++) - { - var bodySize = checked((int)ReadUleb(section, ref pos)); - var bodyEnd = checked(pos + bodySize); - if (bodyEnd > section.Length) - throw new InvalidDataException("The WebAssembly function body extends past the code section."); + var annotated = info.Symbols + .Take(512) + .Select(symbol => NativeDisassembler.DisassembleSymbol(analyzer, symbol)) + .Where(static result => result is not null) + .SelectMany(static result => result!.Value.Instructions) + .Where(static instruction => instruction.OperandText.Contains('<', StringComparison.Ordinal)) + .ToList(); - var localDeclCount = checked((int)ReadUleb(section, ref pos)); - for (var local = 0; local < localDeclCount; local++) - { - _ = ReadUleb(section, ref pos); - _ = ReadByte(section, ref pos); - } + Assert.Contains(annotated, static instruction => + instruction.Mnemonic is "call" or "return_call" + && instruction.TargetName is not null); + Assert.Contains(annotated, static instruction => + instruction.Mnemonic is "local.get" or "local.set" or "local.tee"); + } - var codeOffset = sectionOffset + pos; - var code = section[pos..bodyEnd].ToArray(); - bodies.Add(new WasmFunctionBody(index, codeOffset, code)); - pos = bodyEnd; - } + /// + /// The Size Map treats a raw Wasm module as native code: function bodies, data segments, and + /// remaining sections are sized from file-backed Wasm payloads rather than managed IL. + /// + [Fact(Timeout = 30_000)] + public void BrowserWasmNativeModule_SizeTreeUsesWasmPayloads() + { + using var analyzer = OpenWasmFixture(); - return bodies; + var tree = SizeAnalyzer.BuildSizeTree(analyzer); + Assert.True(tree.Name.EndsWith("(Wasm)", StringComparison.Ordinal), tree.Name); + Assert.True(tree.Size > 0); + Assert.Contains(tree.Children, static child => child.Name == "Functions" && child.Size > 0); + Assert.Contains(tree.Children, static child => child.Name == "Data" && child.Size > 0); + Assert.Contains(tree.Children, static child => child.Name == "Sections" && child.Size > 0); } - private static byte ReadByte(ReadOnlySpan bytes, ref int pos) + private AssemblyAnalyzer OpenWasmFixture() { - if ((uint)pos >= (uint)bytes.Length) - throw new InvalidDataException("Unexpected end of WebAssembly data."); + Assert.SkipWhen( + samples.WasmConsoleNativeWasm is null && samples.ReadyToRunConsoleWasmNativeWasm is null, + "browser-wasm publish did not run on this leg."); - return bytes[pos++]; + return new AssemblyAnalyzer(samples.WasmConsoleNativeWasm ?? samples.ReadyToRunConsoleWasmNativeWasm!); } - private static ulong ReadUleb(ReadOnlySpan bytes, ref int pos) + private AssemblyAnalyzer OpenWebcilFixture() { - ulong value = 0; - var shift = 0; - while (true) + Assert.SkipWhen( + samples.WasmConsoleWebcilWasm is null, + "browser-wasm publish did not produce the Webcil app assembly on this leg."); + + return new AssemblyAnalyzer(samples.WasmConsoleWebcilWasm!); + } + + private static NativeSymbol FindFunctionWithNamedCall(AssemblyAnalyzer analyzer) + { + var info = analyzer.NativeSymbols; + Assert.NotNull(info); + foreach (var symbol in info.Symbols.Take(512)) { - var b = ReadByte(bytes, ref pos); - value |= (ulong)(b & 0x7F) << shift; - if ((b & 0x80) == 0) - return value; - - shift += 7; - if (shift >= 64) - throw new InvalidDataException("The WebAssembly LEB128 value is too large."); + var result = NativeDisassembler.DisassembleSymbol(analyzer, symbol); + if (result is null) + continue; + + if (result.Value.Instructions.Any(static instruction => + instruction.Mnemonic is "call" or "return_call" + && instruction.TargetName is not null)) + { + return symbol; + } } + + throw new InvalidOperationException("No Wasm function with a named direct call was found."); } - private readonly record struct WasmFunctionBody(int Index, int CodeOffset, byte[] Code); + private static void AssertParsedWhenSectionExists(WasmModuleInfo wasm, byte sectionId, int parsedCount, string name) + { + if (wasm.Sections.Any(s => s.Id == sectionId)) + Assert.True(parsedCount > 0, $"Expected parsed {name} entries for section {sectionId}."); + } + + private static void AssertDefinedIndexesStartAfterImports(WasmModuleInfo wasm) + { + AssertIndexesStartAfterImports( + wasm.Imports.Count(static i => i.Kind == WasmExternalKind.Table), + wasm.Tables.Select(static t => t.Index), + "table"); + AssertIndexesStartAfterImports( + wasm.Imports.Count(static i => i.Kind == WasmExternalKind.Memory), + wasm.Memories.Select(static m => m.Index), + "memory"); + AssertIndexesStartAfterImports( + wasm.Imports.Count(static i => i.Kind == WasmExternalKind.Global), + wasm.Globals.Select(static g => g.Index), + "global"); + AssertIndexesStartAfterImports( + wasm.Imports.Count(static i => i.Kind == WasmExternalKind.Tag), + wasm.Tags.Select(static t => t.Index), + "tag"); + } + + private static void AssertIndexesStartAfterImports(int importCount, IEnumerable indexes, string label) + { + var values = indexes.ToList(); + if (values.Count == 0) + return; + + Assert.All(values, index => + Assert.True(index >= importCount, $"{label} index {index} should account for {importCount} imports.")); + } } From 491d1e784430b8cb809e6c3d788a231b59f69f16 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 7 Jul 2026 15:32:46 -0700 Subject: [PATCH 2/2] Fix General tab summary sizing --- src/Dotsider/Views/GeneralView.cs | 220 +++++++++++------- tests/Dotsider.Tests/StandardModeViewTests.cs | 41 +++- 2 files changed, 172 insertions(+), 89 deletions(-) diff --git a/src/Dotsider/Views/GeneralView.cs b/src/Dotsider/Views/GeneralView.cs index b9f9b542..4bd98c21 100644 --- a/src/Dotsider/Views/GeneralView.cs +++ b/src/Dotsider/Views/GeneralView.cs @@ -16,6 +16,11 @@ namespace Dotsider.Views; public static class GeneralView { private static readonly Hex1bColor LabelColor = Hex1bColor.FromRgb(100, 130, 160); + private const int RichSummaryHeightThreshold = 20; + private const int CompactSummaryViewportHeight = 10; + private const int StandardSummaryViewportHeight = 16; + private const int StandardSummaryTerminalHeight = 24; + private const int ReferencesReserveHeight = 12; /// /// Builds the General view widget tree. @@ -78,7 +83,11 @@ public static Hex1bWidget Build(WidgetContext ctx, DotsiderState s $" Target Framework: {state.EffectiveTargetFrameworkDisplay}", $" Culture: {analyzer.Culture ?? "neutral"}", $" Public Key Token: {analyzer.PublicKeyToken ?? "(none)"}", - "", + }; + + var binaryLines = new List(); + var fileLines = new List + { $" File Size: {state.FormatSizeToggleable(analyzer.FileSize)}", $" Architecture: {analyzer.Architecture}", $" Last Modified: {analyzer.LastModified:yyyy-MM-dd HH:mm:ss UTC}", @@ -92,21 +101,20 @@ public static Hex1bWidget Build(WidgetContext ctx, DotsiderState s if (analyzer.NativeAotInfo is { } aot) { var imports = analyzer.Imports; - infoLines.Add(""); - infoLines.Add(" Binary Kind: Native AOT (.NET)"); - infoLines.Add($" ILC / RTR Format: v{aot.MajorVersion}.{aot.MinorVersion} " + binaryLines.Add(" Binary Kind: Native AOT (.NET)"); + binaryLines.Add($" ILC / RTR Format: v{aot.MajorVersion}.{aot.MinorVersion} " + $"({aot.SectionCount} sections @ 0x{aot.HeaderOffset:X})"); - infoLines.Add($" Runtime Version: {aot.RuntimeVersion ?? "(not detected)"}"); - infoLines.Add($" Native Imports: {imports.Count} modules, " + binaryLines.Add($" Runtime Version: {aot.RuntimeVersion ?? "(not detected)"}"); + binaryLines.Add($" Native Imports: {imports.Count} modules, " + $"{imports.Sum(m => m.Functions.Count)} functions"); - infoLines.Add($" R2R Sections: {analyzer.ReadyToRunSections.Count}"); + binaryLines.Add($" R2R Sections: {analyzer.ReadyToRunSections.Count}"); var recoveredTypes = analyzer.RecoveredTypes; - infoLines.Add($" Recovered Types: {recoveredTypes.Count} types, " + binaryLines.Add($" Recovered Types: {recoveredTypes.Count} types, " + $"{recoveredTypes.Sum(t => t.MethodNames.Count)} methods"); - infoLines.Add($" Frozen Strings: {analyzer.FrozenStrings.Count}"); + binaryLines.Add($" Frozen Strings: {analyzer.FrozenStrings.Count}"); if (analyzer.NativeSymbols is { } symbols) { - infoLines.Add(symbols.Symbols.Count > 0 + binaryLines.Add(symbols.Symbols.Count > 0 ? $" Native Symbols: {symbols.Symbols.Count} from {symbols.Source}" : $" Native Symbols: {symbols.Diagnostic ?? symbols.Status.ToString()}"); } @@ -115,13 +123,13 @@ public static Hex1bWidget Build(WidgetContext ctx, DotsiderState s { state.EnsureManagedNativeIndexAsync(); var localCount = companions.LocalReferences.Count; - infoLines.Add(""); - infoLines.Add($" Pre-ILC Sidecars: {companions.Root.FileName}" + binaryLines.Add(""); + binaryLines.Add($" Pre-ILC Sidecars: {companions.Root.FileName}" + (localCount > 0 ? $" (+{localCount} local ref{(localCount == 1 ? "" : "s")})" : "")); - infoLines.Add($" Sidecar Version: {companions.Root.AssemblyVersion ?? "(none)"}" + binaryLines.Add($" Sidecar Version: {companions.Root.AssemblyVersion ?? "(none)"}" + $" ({companions.Root.TargetFramework ?? "unknown TFM"})"); - infoLines.Add($" Sidecar PDB: {analyzer.PreIlcSidecars?.PdbStatus.ToString() ?? "unknown"}"); - infoLines.Add(state.PreIlcIndex is { } index + binaryLines.Add($" Sidecar PDB: {analyzer.PreIlcSidecars?.PdbStatus.ToString() ?? "unknown"}"); + binaryLines.Add(state.PreIlcIndex is { } index ? $" Correlation: {index.ExactCount} of {index.Methods.Count} methods in native image" + $" ({index.AmbiguousCount} ambiguous, {index.MstatOnlyCount} size-only," + $" {index.NotInImageCount} trimmed/inlined)" @@ -129,68 +137,74 @@ public static Hex1bWidget Build(WidgetContext ctx, DotsiderState s } else if (analyzer.PreIlcSidecars is { HasAttachableCompanion: true } offer) { - infoLines.Add(""); - infoLines.Add($" Pre-ILC Sidecars: found ({Path.GetFileName(offer.ManagedAssemblyPath!)})" + binaryLines.Add(""); + binaryLines.Add($" Pre-ILC Sidecars: found ({Path.GetFileName(offer.ManagedAssemblyPath!)})" + " — press a to attach"); } } else if (analyzer.ReadyToRunInfo is { } r2r) { - infoLines.Add(""); - infoLines.Add(" Binary Kind: ReadyToRun (.NET)"); - infoLines.Add($" R2R Format: v{r2r.MajorVersion}.{r2r.MinorVersion} ({r2r.Status})"); - infoLines.Add($" Composite: {r2r.IsComposite}" + binaryLines.Add(" Binary Kind: ReadyToRun (.NET)"); + binaryLines.Add($" R2R Format: v{r2r.MajorVersion}.{r2r.MinorVersion} ({r2r.Status})"); + binaryLines.Add($" Composite: {r2r.IsComposite}" + (r2r.IsComponent ? " (component)" : "") + (r2r.IsPartialImage ? ", partial image" : "")); if (r2r.OwnerCompositeExecutable is { } owner) - infoLines.Add($" Owner Composite: {owner}"); - infoLines.Add($" R2R Sections: {analyzer.ReadyToRunSections.Count}"); + binaryLines.Add($" Owner Composite: {owner}"); + binaryLines.Add($" R2R Sections: {analyzer.ReadyToRunSections.Count}"); if (analyzer.ReadyToRunIndex is { } index) - infoLines.Add($" Precompiled: {index.Methods.Count} methods, " + binaryLines.Add($" Precompiled: {index.Methods.Count} methods, " + $"{index.InstantiationCount} instantiations, {DotsiderState.FormatSize(index.TotalCodeSize)}"); if (analyzer.NativeSymbols is { } symbols) - infoLines.Add(symbols.Symbols.Count > 0 + binaryLines.Add(symbols.Symbols.Count > 0 ? $" Native Symbols: {symbols.Symbols.Count} from {symbols.Source}" : $" Native Symbols: {symbols.Diagnostic ?? symbols.Status.ToString()}"); if (r2r.Diagnostic is { } diagnostic) - infoLines.Add($" Note: {diagnostic}"); + binaryLines.Add($" Note: {diagnostic}"); } else if (analyzer.WasmModuleInfo is { } wasm) { - infoLines.Add(""); - infoLines.Add(" Binary Kind: WebAssembly (.NET)"); - infoLines.Add($" Wasm Version: {wasm.Version}"); - infoLines.Add($" Wasm Sections: {wasm.Sections.Count}"); - infoLines.Add($" Types: {wasm.Types.Count}"); - infoLines.Add($" Functions: {wasm.DefinedFunctionCount} defined, {wasm.ImportedFunctionCount} imported"); - infoLines.Add($" Code Size: {DotsiderState.FormatSize(wasm.CodeSize)}"); - infoLines.Add($" Tables/Memories: {wasm.Tables.Count} / {wasm.Memories.Count}"); - infoLines.Add($" Globals/Elements: {wasm.Globals.Count} / {wasm.Elements.Count}"); - infoLines.Add($" Data Segments: {wasm.DataSegments.Count}, {DotsiderState.FormatSize(wasm.DataSize)}"); + binaryLines.Add(" Binary Kind: WebAssembly (.NET)"); + binaryLines.Add($" Wasm Version: {wasm.Version}"); + binaryLines.Add($" Wasm Sections: {wasm.Sections.Count}"); + binaryLines.Add($" Types: {wasm.Types.Count}"); + binaryLines.Add($" Functions: {wasm.DefinedFunctionCount} defined, {wasm.ImportedFunctionCount} imported"); + binaryLines.Add($" Code Size: {DotsiderState.FormatSize(wasm.CodeSize)}"); + binaryLines.Add($" Tables/Memories: {wasm.Tables.Count} / {wasm.Memories.Count}"); + binaryLines.Add($" Globals/Elements: {wasm.Globals.Count} / {wasm.Elements.Count}"); + binaryLines.Add($" Data Segments: {wasm.DataSegments.Count}, {DotsiderState.FormatSize(wasm.DataSize)}"); if (wasm.StartFunctionIndex is { } start) - infoLines.Add($" Start Function: func:{start}"); - infoLines.Add($" Imports: {wasm.Imports.Count}"); - infoLines.Add($" Exports: {wasm.Exports.Count}"); - infoLines.Add($" Symbol Map: {wasm.SymbolMapStatus}" + binaryLines.Add($" Start Function: func:{start}"); + binaryLines.Add($" Imports: {wasm.Imports.Count}"); + binaryLines.Add($" Exports: {wasm.Exports.Count}"); + binaryLines.Add($" Symbol Map: {wasm.SymbolMapStatus}" + (wasm.SymbolMapEntryCount > 0 ? $" ({wasm.SymbolMapEntryCount} names)" : "")); if (wasm.SymbolMapPath is { } symbolMapPath) - infoLines.Add($" Symbol Map Path: {symbolMapPath}"); + binaryLines.Add($" Symbol Map Path: {symbolMapPath}"); if (analyzer.NativeSymbols is { } symbols) - infoLines.Add(symbols.Symbols.Count > 0 + binaryLines.Add(symbols.Symbols.Count > 0 ? $" Native Symbols: {symbols.Symbols.Count} from {symbols.Source}" : $" Native Symbols: {symbols.Diagnostic ?? symbols.Status.ToString()}"); if (wasm.Diagnostic is { } diagnostic) - infoLines.Add($" Note: {diagnostic}"); + binaryLines.Add($" Note: {diagnostic}"); } else if (analyzer.WebcilInfo is { } webcil) + { + binaryLines.Add(" Binary Kind: Managed Webcil (.NET)"); + binaryLines.Add($" Webcil Format: v{webcil.VersionMajor}.{webcil.VersionMinor}"); + binaryLines.Add($" Wasm Wrapped: {(webcil.IsWasmWrapped ? "Yes" : "No")}"); + binaryLines.Add($" Webcil Sections: {webcil.SectionCount}"); + binaryLines.Add($" Webcil Metadata: {DotsiderState.FormatSize(webcil.MetadataSize)}"); + } + + if (binaryLines.Count > 0) { infoLines.Add(""); - infoLines.Add(" Binary Kind: Managed Webcil (.NET)"); - infoLines.Add($" Webcil Format: v{webcil.VersionMajor}.{webcil.VersionMinor}"); - infoLines.Add($" Wasm Wrapped: {(webcil.IsWasmWrapped ? "Yes" : "No")}"); - infoLines.Add($" Webcil Sections: {webcil.SectionCount}"); - infoLines.Add($" Webcil Metadata: {DotsiderState.FormatSize(webcil.MetadataSize)}"); + infoLines.AddRange(binaryLines); } + infoLines.Add(""); + infoLines.AddRange(fileLines); + var infoText = string.Join("\n", infoLines); // Border chrome adds 2 rows. The AOT layout gets one more so the editor's @@ -217,45 +231,38 @@ public static Hex1bWidget Build(WidgetContext ctx, DotsiderState s ref state.GeneralInfoPrevCursorPosition); } - return ctx.VStack(outer => + static Hex1bWidget BuildInfoPanel( + WidgetContext outer, + DotsiderState state) { - var infoPanel = - outer.Border( - outer.ThemePanel(t => t - .Set(EditorTheme.SelectionForegroundColor, Hex1bColor.Default) - .Set(EditorTheme.SelectionBackgroundColor, Hex1bColor.FromRgb(79, 82, 88)), - outer.Editor(state.GeneralInfoEditorState!) - .ViewRenderer(InfoEditorViewRenderer.Instance) - .Decorations(new InfoLabelDecorationProvider()) - .Decorations(state.GeneralInfoYankProvider) - .InputBindings(bindings => - { - TextObjectHelper.ConfigureReadOnlyEditorBindings( - bindings, - state.GeneralInfoEditorState!, - () => state.VimPending, - () => state.VimPendingEditor, - () => state.VimPendingCursorOffset, - () => state.VimPendingTimestamp, - (s, e, o) => { state.VimPending = s; state.VimPendingEditor = e; state.VimPendingCursorOffset = o; state.VimPendingTimestamp = DateTime.UtcNow; }, - state.PerformEditorYank, - () => state.App.Invalidate()); - }) - .FillWidth().FillHeight()) - ).Title(" Assembly Info "); - - var widgets = new List - { - // Rich binary summaries can outgrow the viewport; split those with - // the references table so neither section disappears after resize. - infoHeight > 20 ? infoPanel.FillHeight(2) : infoPanel.FixedHeight(infoHeight) - }; - - // Search bar - SearchBarHelper.AddSearchBar(widgets, outer, search, state.App); + return outer.Border( + outer.ThemePanel(t => t + .Set(EditorTheme.SelectionForegroundColor, Hex1bColor.Default) + .Set(EditorTheme.SelectionBackgroundColor, Hex1bColor.FromRgb(79, 82, 88)), + outer.Editor(state.GeneralInfoEditorState!) + .ViewRenderer(InfoEditorViewRenderer.Instance) + .Decorations(new InfoLabelDecorationProvider()) + .Decorations(state.GeneralInfoYankProvider) + .InputBindings(bindings => + { + TextObjectHelper.ConfigureReadOnlyEditorBindings( + bindings, + state.GeneralInfoEditorState!, + () => state.VimPending, + () => state.VimPendingEditor, + () => state.VimPendingCursorOffset, + () => state.VimPendingTimestamp, + (s, e, o) => { state.VimPending = s; state.VimPendingEditor = e; state.VimPendingCursorOffset = o; state.VimPendingTimestamp = DateTime.UtcNow; }, + state.PerformEditorYank, + () => state.App.Invalidate()); + }) + .FillWidth().FillHeight()) + ).Title(" Assembly Info "); + } - // Assembly References table - widgets.Add(outer.Border( + Hex1bWidget BuildReferencesPanel(WidgetContext outer) + { + return outer.Border( outer.Table(refs) .RowKey(r => r.Name) .Header(h => @@ -329,10 +336,47 @@ public static Hex1bWidget Build(WidgetContext ctx, DotsiderState s }) ).Title(routed ? $" Assembly References (pre-ILC) ({refs.Count}) " - : $" Assembly References ({refs.Count}) ").Fill()); + : $" Assembly References ({refs.Count}) ").Fill(); + } - return [.. widgets]; - }) + Hex1bWidget BuildGeneralStack(WidgetContext outer, int panelHeight) where T : Hex1bWidget + { + return outer.VStack(v => + { + var widgets = new List + { + BuildInfoPanel(v, state).FixedHeight(panelHeight) + }; + + SearchBarHelper.AddSearchBar(widgets, v, search, state.App); + widgets.Add(BuildReferencesPanel(v)); + + return [.. widgets]; + }).Fill(); + } + + var compactInfoHeight = infoHeight > RichSummaryHeightThreshold + ? Math.Min(infoHeight, CompactSummaryViewportHeight) + : infoHeight; + var standardInfoHeight = infoHeight > RichSummaryHeightThreshold + ? Math.Min(infoHeight, StandardSummaryViewportHeight) + : infoHeight; + + // Decide the rich-summary height at the root of the General view, where Hex1b + // has the real viewport height. This keeps tall terminals content-sized while + // short terminals reserve space for the references table. + Hex1bWidget content = infoHeight > RichSummaryHeightThreshold + ? ctx.Responsive(r => + [ + r.When((_, height) => height >= infoHeight + ReferencesReserveHeight, + outer => BuildGeneralStack(outer, infoHeight)), + r.When((_, height) => height >= StandardSummaryTerminalHeight, + outer => BuildGeneralStack(outer, standardInfoHeight)), + r.Otherwise(outer => BuildGeneralStack(outer, compactInfoHeight)) + ]).Fill() + : BuildGeneralStack(ctx, infoHeight); + + return content .InputBindings(bindings => { bindings.Key(Hex1bKey.Tab).Global().Action(_ => diff --git a/tests/Dotsider.Tests/StandardModeViewTests.cs b/tests/Dotsider.Tests/StandardModeViewTests.cs index f075b089..a9c9b19a 100644 --- a/tests/Dotsider.Tests/StandardModeViewTests.cs +++ b/tests/Dotsider.Tests/StandardModeViewTests.cs @@ -279,6 +279,45 @@ public async Task General_Wasm_ShortTerminal_KeepsAssemblyReferencesVisible() await runTask; } + /// + /// Verifies the Wasm General tab does not reserve a large blank area + /// between the content-sized summary and the references section. + /// + [Fact(Timeout = 30_000)] + public async Task General_Wasm_TallTerminal_DoesNotPadBeforeAssemblyReferences() + { + var wasmPath = GetWasmNativePath(); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken); + var (terminal, app) = CreateDotsiderAppWithDimensions(wasmPath, width: 160, height: 50); + var runTask = app.RunAsync(cts.Token); + await Task.Delay(100, cts.Token); + + int sourceLinkLine = -1; + int refsLine = -1; + await new Hex1bTerminalInputSequenceBuilder() + .WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10)) + .WaitUntil(s => s.ContainsText("dotnet.native.wasm"), TimeSpan.FromSeconds(10)) + .WaitUntil(s => + { + var sourceLink = s.FindText("Source Link"); + var references = s.FindText("Assembly References"); + if (sourceLink.Count == 0 || references.Count == 0) + return false; + + sourceLinkLine = sourceLink[0].Line; + refsLine = references[0].Line; + return true; + }, TimeSpan.FromSeconds(10)) + .Build() + .ApplyAsync(terminal, cts.Token); + + Assert.InRange(refsLine - sourceLinkLine, 1, 4); + + cts.Cancel(); + await runTask; + } + /// /// Verifies the PE/Metadata tab routes raw Wasm modules to WebAssembly section rows. /// @@ -3295,7 +3334,6 @@ public async Task General_NativeAot_ShowsAotInfo() .WaitUntil(s => s.ContainsText("Native Imports"), TimeSpan.FromSeconds(10)) .WaitUntil(s => s.ContainsText("R2R Sections"), TimeSpan.FromSeconds(10)) .WaitUntil(s => s.ContainsText("Recovered Types"), TimeSpan.FromSeconds(10)) - .WaitUntil(s => s.ContainsText("Native Symbols"), TimeSpan.FromSeconds(10)) .Build() .ApplyAsync(terminal, cts.Token); @@ -3304,6 +3342,7 @@ public async Task General_NativeAot_ShowsAotInfo() Assert.NotEmpty(_state.Analyzer.ReadyToRunSections); Assert.NotEmpty(_state.Analyzer.RecoveredTypes); Assert.NotNull(_state.Analyzer.NativeSymbols); + Assert.NotEmpty(_state.Analyzer.NativeSymbols.Symbols); cts.Cancel(); await runTask;