Browse Source

ResourcesFileTreeNode (.resources file dispatch + entries)

Assisted-by: Claude:claude-opus-4-7:Claude Code

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
e22ef363b6
  1. 24
      ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs
  2. 1
      ILSpy/Assets/Icons/ResourceResourcesFile.svg
  3. 1
      ILSpy/Images.cs
  4. 59
      ILSpy/TreeNodes/ResourceEntryNode.cs
  5. 8
      ILSpy/TreeNodes/ResourceTreeNode.cs
  6. 94
      ILSpy/TreeNodes/ResourcesFileTreeNode.cs

24
ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs

@ -65,6 +65,30 @@ public class AssemblyTreeTests @@ -65,6 +65,30 @@ public class AssemblyTreeTests
resources.Children.Should().AllBeAssignableTo<ResourceTreeNode>();
}
[AvaloniaTest]
public async Task Dot_Resources_File_Resolves_To_ResourcesFileTreeNode()
{
var window = AppComposition.Current.GetExport<MainWindow>();
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<AssemblyTreeNode>(coreLibName);
assemblyNode.EnsureLazyChildren();
var resources = assemblyNode.Children.OfType<ResourceListTreeNode>().Single();
resources.EnsureLazyChildren();
var resourceFileNode = resources.Children.OfType<ResourcesFileTreeNode>().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()
{

1
ILSpy/Assets/Icons/ResourceResourcesFile.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.st0{opacity:0;fill:#424242}.st1{fill:#f6f6f6}.st2{fill:#424242}.st3{fill:#f0eff1}.st4{fill:#424242;stroke:#424242;stroke-width:.2;stroke-miterlimit:10}.st5{fill:#efeef0}</style><g id="outline"><path class="st0" d="M0 0h16v16H0z"/><path class="st1" d="M12.982 0H7.941C7.001 0 6 .701 6 2H2.941C2.001 2 1 2.701 1 4v3H0v9h12.942c1.426 0 2.011-1.161 2.059-2V8.875h1V2.688L12.982 0z"/></g><g id="icon_x5F_bg"><circle class="st2" cx="5.75" cy="10.25" r=".75"/><path class="st2" d="M1 8v7h7V8H1zm1 6l1.5-1.5 2 1.5H2zm5-.5L3.5 11 2 12.5V9h5v4.5zM15.023 3.136L12.625 1h-4.66s-.941 0-.941 1h5v2h2v3.241l.755.635h.245v-4.74zM3.023 7V4h4v1H9.87L7.625 3h-4.66s-.941 0-.941 1v3h.999z"/><path class="st2" d="M11.625 6h-4.66s-.941 0-.941 1H11.023v2h2v5H9v1h3.965c1 0 1.059-1 1.059-1V8.137L11.625 6z"/></g><g id="icon_x5F_fg"><path class="st3" d="M2 14h3.5l-2-1.5z"/><path class="st3" d="M2 9v3.5L3.5 11 7 13.5V9H2zm3.75 2a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5z"/><circle class="st4" cx="5.75" cy="10.25" r=".75"/><path class="st5" d="M8 2h3.936v1.96L14 3.996v3.225L8.023 2M5 7c0-1.27.986-1.964 1.936-1.994V4H3v3h2zM10.936 8.96V7H9v7h4V8.996z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

1
ILSpy/Images.cs

@ -80,6 +80,7 @@ namespace ILSpy.Images @@ -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));

59
ILSpy/TreeNodes/ResourceEntryNode.cs

@ -0,0 +1,59 @@ @@ -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
{
/// <summary>
/// One entry inside a <see cref="ResourcesFileTreeNode"/>'s <c>.resources</c> file —
/// a key plus an opener for the underlying byte payload.
/// </summary>
public class ResourceEntryNode : ILSpyTreeNode
{
readonly string key;
readonly Func<Stream> openStream;
public ResourceEntryNode(string key, Func<Stream> 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));
}
}

8
ILSpy/TreeNodes/ResourceTreeNode.cs

@ -51,6 +51,12 @@ namespace ILSpy.TreeNodes @@ -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);
}
}
}

94
ILSpy/TreeNodes/ResourcesFileTreeNode.cs

@ -0,0 +1,94 @@ @@ -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
{
/// <summary>
/// A <c>.resources</c> file embedded in an assembly. Children are
/// <see cref="ResourceEntryNode"/> for byte-payload entries; string entries are
/// captured into <see cref="StringTableEntries"/> for inline rendering during
/// decompilation.
/// </summary>
public sealed class ResourcesFileTreeNode : ResourceTreeNode
{
readonly List<KeyValuePair<string, string>> stringTableEntries = new();
public IReadOnlyList<KeyValuePair<string, string>> 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<string, object?> entry)
{
switch (entry.Value)
{
case string s:
stringTableEntries.Add(new KeyValuePair<string, string>(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}");
}
}
}
Loading…
Cancel
Save