diff --git a/ILSpy.Tests/Metadata/HeapTreeTests.cs b/ILSpy.Tests/Metadata/HeapTreeTests.cs new file mode 100644 index 000000000..9e4d3fa62 --- /dev/null +++ b/ILSpy.Tests/Metadata/HeapTreeTests.cs @@ -0,0 +1,145 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Linq; +using System.Threading.Tasks; + +using Avalonia.Headless.NUnit; + +using AwesomeAssertions; + +using ILSpy.AppEnv; +using ILSpy.Metadata; +using ILSpy.TreeNodes; +using ILSpy.ViewModels; +using ILSpy.Views; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests.Metadata; + +[TestFixture] +public class HeapTreeTests +{ + [AvaloniaTest] + public async Task MetadataTreeNode_Surfaces_Four_Heap_Children_For_A_PE_Assembly() + { + // CLI metadata divides ancillary data into four heaps: #Strings (member/type names), + // #US (literal user strings emitted by ldstr), #GUID (16-byte module/type IDs), and + // #Blob (signatures, marshalling info, custom-attr blobs). All four should be + // reachable from the Metadata folder regardless of whether the file's debug + // metadata is present. + + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + var coreLibName = typeof(object).Assembly.GetName().Name!; + var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + assemblyNode.EnsureLazyChildren(); + var metadataNode = assemblyNode.Children.OfType().Single(); + metadataNode.EnsureLazyChildren(); + + metadataNode.Children.OfType().Should().ContainSingle(); + metadataNode.Children.OfType().Should().ContainSingle(); + metadataNode.Children.OfType().Should().ContainSingle(); + metadataNode.Children.OfType().Should().ContainSingle(); + } + + [AvaloniaTest] + public async Task StringHeapTreeNode_Decompiles_Header_Plus_Preview_Of_Entries() + { + // CoreLib's #Strings holds tens of thousands of entries; rendering all of them as + // text would bloat the tab and bog down the editor. Phase 1 dumps a capped preview + // (header + first N rows + truncation footer) so the user can scan the start of the + // heap; Phase 2 swaps to a virtualising DataGrid for the full set. + + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + var coreLibName = typeof(object).Assembly.GetName().Name!; + var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + assemblyNode.EnsureLazyChildren(); + var metadataNode = assemblyNode.Children.OfType().Single(); + metadataNode.EnsureLazyChildren(); + var heapNode = metadataNode.Children.OfType().Single(); + + vm.AssemblyTreeModel.SelectNode(heapNode); + var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + tab.Text.Should().Contain("String Heap"); + tab.Text.Should().Contain("Offset"); + tab.Text.Should().Contain("Length"); + tab.Text.Should().Contain("Value"); + tab.Text.Should().Contain("more entries", "CoreLib's #Strings is well over the preview cap"); + // The preview includes a (count entries) header — should match e.g. "(24573 entries)". + tab.Text.Should().MatchRegex(@"\(\d{3,} entries\)"); + } + + [AvaloniaTest] + public async Task GuidHeapTreeNode_Decompiles_Header_Plus_Index_Length_Value_Columns() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + var coreLibName = typeof(object).Assembly.GetName().Name!; + var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + assemblyNode.EnsureLazyChildren(); + var metadataNode = assemblyNode.Children.OfType().Single(); + metadataNode.EnsureLazyChildren(); + var heapNode = metadataNode.Children.OfType().Single(); + + vm.AssemblyTreeModel.SelectNode(heapNode); + var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + tab.Text.Should().Contain("Guid Heap"); + tab.Text.Should().Contain("Index"); + tab.Text.Should().Contain("Length"); + tab.Text.Should().MatchRegex(@"\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b", + "the GUID heap should contain at least one parsed GUID"); + } + + [AvaloniaTest] + public async Task BlobHeapTreeNode_Decompiles_Header_Plus_Preview_Of_Hex_Encoded_Entries() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + var coreLibName = typeof(object).Assembly.GetName().Name!; + var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + assemblyNode.EnsureLazyChildren(); + var metadataNode = assemblyNode.Children.OfType().Single(); + metadataNode.EnsureLazyChildren(); + var heapNode = metadataNode.Children.OfType().Single(); + + vm.AssemblyTreeModel.SelectNode(heapNode); + var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + tab.Text.Should().Contain("Blob Heap"); + tab.Text.Should().Contain("Offset"); + tab.Text.Should().Contain("Length"); + tab.Text.Should().Contain("Value"); + } +} diff --git a/ILSpy/Metadata/BlobHeapTreeNode.cs b/ILSpy/Metadata/BlobHeapTreeNode.cs new file mode 100644 index 000000000..ed7bea309 --- /dev/null +++ b/ILSpy/Metadata/BlobHeapTreeNode.cs @@ -0,0 +1,93 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Metadata; + +using ILSpy.Languages; + +namespace ILSpy.Metadata +{ + /// + /// View of #Blob — variable-length byte payloads holding signatures, marshalling info, + /// custom-attribute values, and similar binary data. Each entry is rendered as a hex + /// dump; full structural decoding (e.g. parsed type signatures) is out of scope for + /// the metadata viewer. + /// + public sealed class BlobHeapTreeNode : MetadataHeapTreeNode + { + List? entries; + + public BlobHeapTreeNode(MetadataFile metadataFile) + : base(HandleKind.Blob, metadataFile) + { + } + + public override object Text => $"Blob Heap ({EnsureEntries().Count})"; + public override string ToString() => "Blob Heap"; + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + var all = EnsureEntries(); + var preview = all.Count > PreviewLimit + ? (IReadOnlyList)all.GetRange(0, PreviewLimit) + : all; + language.WriteCommentLine(output, $"Blob Heap ({all.Count} entries)"); + MetadataTextWriter.WriteTable(language, output, preview); + if (all.Count > preview.Count) + language.WriteCommentLine(output, $"... ({all.Count - preview.Count} more entries; full view ships with the metadata DataGrid in a future release)"); + } + + List EnsureEntries() + { + if (entries is { } cached) + return cached; + var list = new List(); + var metadata = metadataFile.Metadata; + var handle = MetadataTokens.BlobHandle(0); + do + { + list.Add(new BlobHeapEntry(metadata, handle)); + handle = metadata.GetNextHandle(handle); + } while (!handle.IsNil); + entries = list; + return list; + } + + public sealed class BlobHeapEntry + { + readonly MetadataReader metadata; + readonly BlobHandle handle; + + [ColumnInfo("X8")] + public int Offset => metadata.GetHeapOffset(handle); + public int Length => metadata.GetBlobReader(handle).Length; + public string Value => metadata.GetBlobReader(handle).ToHexString(); + + public BlobHeapEntry(MetadataReader metadata, BlobHandle handle) + { + this.metadata = metadata; + this.handle = handle; + } + } + } +} diff --git a/ILSpy/Metadata/GuidHeapTreeNode.cs b/ILSpy/Metadata/GuidHeapTreeNode.cs new file mode 100644 index 000000000..904b119a3 --- /dev/null +++ b/ILSpy/Metadata/GuidHeapTreeNode.cs @@ -0,0 +1,88 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Metadata; + +using ILSpy.Languages; + +namespace ILSpy.Metadata +{ + /// + /// View of #GUID — a contiguous array of 16-byte GUIDs indexed (1-based) by ModuleDef + /// rows and a handful of other tables. Indexing starts at 1 because index 0 is reserved + /// for "no GUID". + /// + public sealed class GuidHeapTreeNode : MetadataHeapTreeNode + { + List? entries; + + public GuidHeapTreeNode(MetadataFile metadataFile) + : base(HandleKind.Guid, metadataFile) + { + } + + public override object Text => $"Guid Heap ({EnsureEntries().Count})"; + public override string ToString() => "Guid Heap"; + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + var all = EnsureEntries(); + var preview = all.Count > PreviewLimit + ? (IReadOnlyList)all.GetRange(0, PreviewLimit) + : all; + language.WriteCommentLine(output, $"Guid Heap ({all.Count} entries)"); + MetadataTextWriter.WriteTable(language, output, preview); + if (all.Count > preview.Count) + language.WriteCommentLine(output, $"... ({all.Count - preview.Count} more entries; full view ships with the metadata DataGrid in a future release)"); + } + + List EnsureEntries() + { + if (entries is { } cached) + return cached; + var list = new List(); + var metadata = metadataFile.Metadata; + int count = metadata.GetHeapSize(HeapIndex.Guid) >> 4; + for (int i = 1; i <= count; i++) + list.Add(new GuidHeapEntry(metadata, MetadataTokens.GuidHandle(i))); + entries = list; + return list; + } + + public sealed class GuidHeapEntry + { + readonly MetadataReader metadata; + readonly GuidHandle handle; + + public int Index => metadata.GetHeapOffset(handle); + public int Length => 16; + public string Value => metadata.GetGuid(handle).ToString(); + + public GuidHeapEntry(MetadataReader metadata, GuidHandle handle) + { + this.metadata = metadata; + this.handle = handle; + } + } + } +} diff --git a/ILSpy/Metadata/MetadataHeapTreeNode.cs b/ILSpy/Metadata/MetadataHeapTreeNode.cs new file mode 100644 index 000000000..5b1ce4700 --- /dev/null +++ b/ILSpy/Metadata/MetadataHeapTreeNode.cs @@ -0,0 +1,51 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Reflection.Metadata; + +using ICSharpCode.Decompiler.Metadata; + +using ILSpy.TreeNodes; + +namespace ILSpy.Metadata +{ + /// + /// Common parent for the four heap views (#Strings, #US, #GUID, #Blob). Holds the + /// shared icon and the that distinguishes them — so token + /// navigation in Phase 3 can dispatch a heap-typed handle to the right node by walking + /// children and matching . Concrete subclasses own row materialisation + /// and rendering because each heap has a different row shape. + /// + public abstract class MetadataHeapTreeNode : ILSpyTreeNode + { + public const int PreviewLimit = 200; + + protected readonly MetadataFile metadataFile; + + public HandleKind Kind { get; } + + protected MetadataHeapTreeNode(HandleKind kind, MetadataFile metadataFile) + { + Kind = kind; + this.metadataFile = metadataFile ?? throw new ArgumentNullException(nameof(metadataFile)); + } + + public override object Icon => Images.Images.MetadataTable; + } +} diff --git a/ILSpy/Metadata/MetadataTreeNode.cs b/ILSpy/Metadata/MetadataTreeNode.cs index 201ce5cd5..f01b79da0 100644 --- a/ILSpy/Metadata/MetadataTreeNode.cs +++ b/ILSpy/Metadata/MetadataTreeNode.cs @@ -102,7 +102,13 @@ namespace ILSpy.Metadata Children.Add(new DebugDirectoryTreeNode(peFile)); } - // Heaps and metadata-tables children land in subsequent commits. + // MetadataTablesTreeNode container slots in here in the next commit, between PE + // headers and heaps to match WPF ordering. + + Children.Add(new StringHeapTreeNode(metadataFile)); + Children.Add(new UserStringHeapTreeNode(metadataFile)); + Children.Add(new GuidHeapTreeNode(metadataFile)); + Children.Add(new BlobHeapTreeNode(metadataFile)); } } } diff --git a/ILSpy/Metadata/StringHeapTreeNode.cs b/ILSpy/Metadata/StringHeapTreeNode.cs new file mode 100644 index 000000000..e61e6d6dd --- /dev/null +++ b/ILSpy/Metadata/StringHeapTreeNode.cs @@ -0,0 +1,93 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Metadata; + +using ILSpy.Languages; + +namespace ILSpy.Metadata +{ + /// + /// View of #Strings — the heap that holds every UTF-8 type/member/namespace name + /// referenced by the metadata tables. Lazy materialisation: handles are walked on + /// demand via the first time a + /// caller asks for the count or a preview row. + /// + public sealed class StringHeapTreeNode : MetadataHeapTreeNode + { + List? entries; + + public StringHeapTreeNode(MetadataFile metadataFile) + : base(HandleKind.String, metadataFile) + { + } + + public override object Text => $"String Heap ({EnsureEntries().Count})"; + public override string ToString() => "String Heap"; + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + var all = EnsureEntries(); + var preview = all.Count > PreviewLimit + ? (IReadOnlyList)all.GetRange(0, PreviewLimit) + : all; + language.WriteCommentLine(output, $"String Heap ({all.Count} entries)"); + MetadataTextWriter.WriteTable(language, output, preview); + if (all.Count > preview.Count) + language.WriteCommentLine(output, $"... ({all.Count - preview.Count} more entries; full view ships with the metadata DataGrid in a future release)"); + } + + List EnsureEntries() + { + if (entries is { } cached) + return cached; + var list = new List(); + var metadata = metadataFile.Metadata; + var handle = MetadataTokens.StringHandle(0); + do + { + list.Add(new StringHeapEntry(metadata, handle)); + handle = metadata.GetNextHandle(handle); + } while (!handle.IsNil); + entries = list; + return list; + } + + public sealed class StringHeapEntry + { + readonly MetadataReader metadata; + readonly StringHandle handle; + + [ColumnInfo("X8")] + public int Offset => metadata.GetHeapOffset(handle); + public int Length => metadata.GetString(handle).Length; + public string Value => metadata.GetString(handle); + + public StringHeapEntry(MetadataReader metadata, StringHandle handle) + { + this.metadata = metadata; + this.handle = handle; + } + } + } +} diff --git a/ILSpy/Metadata/UserStringHeapTreeNode.cs b/ILSpy/Metadata/UserStringHeapTreeNode.cs new file mode 100644 index 000000000..09efbc9df --- /dev/null +++ b/ILSpy/Metadata/UserStringHeapTreeNode.cs @@ -0,0 +1,92 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Metadata; + +using ILSpy.Languages; + +namespace ILSpy.Metadata +{ + /// + /// View of #US — the UTF-16 heap holding every literal string emitted by an ldstr + /// instruction. Distinct from #Strings (which carries identifier names); user strings + /// are referenced by metadata tokens, not by member-name resolution. + /// + public sealed class UserStringHeapTreeNode : MetadataHeapTreeNode + { + List? entries; + + public UserStringHeapTreeNode(MetadataFile metadataFile) + : base(HandleKind.UserString, metadataFile) + { + } + + public override object Text => $"UserString Heap ({EnsureEntries().Count})"; + public override string ToString() => "UserString Heap"; + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + var all = EnsureEntries(); + var preview = all.Count > PreviewLimit + ? (IReadOnlyList)all.GetRange(0, PreviewLimit) + : all; + language.WriteCommentLine(output, $"UserString Heap ({all.Count} entries)"); + MetadataTextWriter.WriteTable(language, output, preview); + if (all.Count > preview.Count) + language.WriteCommentLine(output, $"... ({all.Count - preview.Count} more entries; full view ships with the metadata DataGrid in a future release)"); + } + + List EnsureEntries() + { + if (entries is { } cached) + return cached; + var list = new List(); + var metadata = metadataFile.Metadata; + var handle = MetadataTokens.UserStringHandle(0); + do + { + list.Add(new UserStringHeapEntry(metadata, handle)); + handle = metadata.GetNextHandle(handle); + } while (!handle.IsNil); + entries = list; + return list; + } + + public sealed class UserStringHeapEntry + { + readonly MetadataReader metadata; + readonly UserStringHandle handle; + + [ColumnInfo("X8")] + public int Offset => metadata.GetHeapOffset(handle); + public int Length => metadata.GetUserString(handle).Length; + public string Value => metadata.GetUserString(handle); + + public UserStringHeapEntry(MetadataReader metadata, UserStringHandle handle) + { + this.metadata = metadata; + this.handle = handle; + } + } + } +}