diff --git a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs index 9a5e56632..9bb04ebdb 100644 --- a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs +++ b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs @@ -65,6 +65,30 @@ public class AssemblyTreeTests resources.Children.Should().AllBeAssignableTo(); } + [AvaloniaTest] + public async Task Dot_Resources_File_Resolves_To_ResourcesFileTreeNode() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var coreLibName = typeof(object).Assembly.GetName().Name!; + var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + assemblyNode.EnsureLazyChildren(); + var resources = assemblyNode.Children.OfType().Single(); + resources.EnsureLazyChildren(); + + var resourceFileNode = resources.Children.OfType().FirstOrDefault(); + ((object?)resourceFileNode).Should().NotBeNull( + "CoreLib must ship at least one embedded .resources file"); + resourceFileNode!.Resource.Name.Should().EndWith(".resources"); + + resourceFileNode.EnsureLazyChildren(); + (resourceFileNode.Children.Count > 0 || resourceFileNode.StringTableEntries.Count > 0) + .Should().BeTrue("a .resources file must surface either inner ResourceEntryNode children or string-table entries"); + } + [AvaloniaTest] public async Task Assembly_Node_Has_References_Folder_Child() { diff --git a/ILSpy/Assets/Icons/ResourceResourcesFile.svg b/ILSpy/Assets/Icons/ResourceResourcesFile.svg new file mode 100644 index 000000000..7f463d7ef --- /dev/null +++ b/ILSpy/Assets/Icons/ResourceResourcesFile.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Images.cs b/ILSpy/Images.cs index 2a754ae7f..457d43c4c 100644 --- a/ILSpy/Images.cs +++ b/ILSpy/Images.cs @@ -80,6 +80,7 @@ namespace ILSpy.Images public static readonly IImage FolderClosed = LoadSvg(nameof(FolderClosed)); public static readonly IImage FolderOpen = LoadSvg(nameof(FolderOpen)); public static readonly IImage Resource = LoadSvg(nameof(Resource)); + public static readonly IImage ResourceResourcesFile = LoadSvg(nameof(ResourceResourcesFile)); // Types public static readonly IImage Class = LoadSvg(nameof(Class)); diff --git a/ILSpy/TreeNodes/ResourceEntryNode.cs b/ILSpy/TreeNodes/ResourceEntryNode.cs new file mode 100644 index 000000000..057b81c8d --- /dev/null +++ b/ILSpy/TreeNodes/ResourceEntryNode.cs @@ -0,0 +1,59 @@ +// 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.IO; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.IL; + +using ILSpy.Languages; + +namespace ILSpy.TreeNodes +{ + /// + /// One entry inside a 's .resources file — + /// a key plus an opener for the underlying byte payload. + /// + public class ResourceEntryNode : ILSpyTreeNode + { + readonly string key; + readonly Func openStream; + + public ResourceEntryNode(string key, Func openStream) + { + this.key = key ?? throw new ArgumentNullException(nameof(key)); + this.openStream = openStream ?? throw new ArgumentNullException(nameof(openStream)); + } + + public override object Text => ILAmbience.EscapeName(key); + + public override object Icon => Images.Images.Resource; + + protected Stream OpenStream() => openStream(); + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + using var data = OpenStream(); + language.WriteCommentLine(output, $"{key} = {data.Length} bytes"); + } + + public static ILSpyTreeNode Create(string name, byte[] data) + => new ResourceEntryNode(name, () => new MemoryStream(data)); + } +} diff --git a/ILSpy/TreeNodes/ResourceTreeNode.cs b/ILSpy/TreeNodes/ResourceTreeNode.cs index 03f1ccab0..64acb49d0 100644 --- a/ILSpy/TreeNodes/ResourceTreeNode.cs +++ b/ILSpy/TreeNodes/ResourceTreeNode.cs @@ -51,6 +51,12 @@ namespace ILSpy.TreeNodes language.WriteCommentLine(output, $"{Resource.Name} ({Resource.ResourceType}, {Resource.Attributes}{sizeInBytesText})"); } - public static ILSpyTreeNode Create(Resource resource) => new ResourceTreeNode(resource); + public static ILSpyTreeNode Create(Resource resource) + { + ArgumentNullException.ThrowIfNull(resource); + if (resource.Name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)) + return new ResourcesFileTreeNode(resource); + return new ResourceTreeNode(resource); + } } } diff --git a/ILSpy/TreeNodes/ResourcesFileTreeNode.cs b/ILSpy/TreeNodes/ResourcesFileTreeNode.cs new file mode 100644 index 000000000..f0999bdbc --- /dev/null +++ b/ILSpy/TreeNodes/ResourcesFileTreeNode.cs @@ -0,0 +1,94 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.Util; + +using ILSpy.Languages; + +namespace ILSpy.TreeNodes +{ + /// + /// A .resources file embedded in an assembly. Children are + /// for byte-payload entries; string entries are + /// captured into for inline rendering during + /// decompilation. + /// + public sealed class ResourcesFileTreeNode : ResourceTreeNode + { + readonly List> stringTableEntries = new(); + + public IReadOnlyList> StringTableEntries => stringTableEntries; + + public ResourcesFileTreeNode(Resource r) : base(r) + { + LazyLoading = true; + } + + public override object Icon => Images.Images.ResourceResourcesFile; + + protected override void LoadChildren() + { + var s = Resource.TryOpenStream(); + if (s == null) + return; + s.Position = 0; + try + { + foreach (var entry in new ResourcesFile(s).OrderBy(e => e.Key, NaturalStringComparer.Instance)) + ProcessEntry(entry); + } + catch (BadImageFormatException) { /* malformed — ignore */ } + catch (EndOfStreamException) { /* truncated — ignore */ } + } + + void ProcessEntry(KeyValuePair entry) + { + switch (entry.Value) + { + case string s: + stringTableEntries.Add(new KeyValuePair(entry.Key, s)); + break; + case byte[] bytes: + Children.Add(ResourceEntryNode.Create(entry.Key, bytes)); + break; + case MemoryStream ms: + Children.Add(ResourceEntryNode.Create(entry.Key, ms.ToArray())); + break; + } + } + + public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) + { + EnsureLazyChildren(); + base.Decompile(language, output, options); + if (stringTableEntries.Count == 0) + return; + output.WriteLine(); + language.WriteCommentLine(output, "string table:"); + foreach (var pair in stringTableEntries) + language.WriteCommentLine(output, $" {pair.Key} = {pair.Value}"); + } + } +}