Skip to content

Commit 775ff37

Browse files
Merge pull request #3730 from siegfriedpammer/ilspycmd-baml-resources
ilspycmd: list/extract embedded resources, decompile BAML to XAML
2 parents aad16c6 + d5e2e61 commit 775ff37

5 files changed

Lines changed: 408 additions & 3 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
4+
// software and associated documentation files (the "Software"), to deal in the Software
5+
// without restriction, including without limitation the rights to use, copy, modify, merge,
6+
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
7+
// to whom the Software is furnished to do so, subject to the following conditions:
8+
//
9+
// The above copyright notice and this permission notice shall be included in all copies or
10+
// substantial portions of the Software.
11+
//
12+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13+
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14+
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
15+
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
17+
// DEALINGS IN THE SOFTWARE.
18+
19+
using System;
20+
using System.Collections.Generic;
21+
using System.IO;
22+
using System.Threading;
23+
24+
using ICSharpCode.BamlDecompiler;
25+
using ICSharpCode.Decompiler;
26+
using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
27+
using ICSharpCode.Decompiler.DebugInfo;
28+
using ICSharpCode.Decompiler.Metadata;
29+
30+
namespace ICSharpCode.ILSpyCmd
31+
{
32+
sealed class BamlAwareWholeProjectDecompiler : WholeProjectDecompiler
33+
{
34+
readonly BamlDecompilerTypeSystem bamlTypeSystem;
35+
readonly BamlDecompilerSettings bamlSettings;
36+
37+
public BamlAwareWholeProjectDecompiler(
38+
DecompilerSettings settings,
39+
IAssemblyResolver assemblyResolver,
40+
AssemblyReferenceClassifier assemblyReferenceClassifier,
41+
IDebugInfoProvider debugInfoProvider,
42+
BamlDecompilerTypeSystem bamlTypeSystem,
43+
BamlDecompilerSettings bamlSettings)
44+
: base(settings, assemblyResolver, projectWriter: null, assemblyReferenceClassifier, debugInfoProvider)
45+
{
46+
this.bamlTypeSystem = bamlTypeSystem ?? throw new ArgumentNullException(nameof(bamlTypeSystem));
47+
this.bamlSettings = bamlSettings ?? throw new ArgumentNullException(nameof(bamlSettings));
48+
}
49+
50+
public CancellationToken CancellationToken { get; set; }
51+
52+
protected override IEnumerable<ProjectItemInfo> WriteResourceToFile(string fileName, string resourceName, Stream entryStream)
53+
{
54+
if (!fileName.EndsWith(".baml", StringComparison.OrdinalIgnoreCase))
55+
return base.WriteResourceToFile(fileName, resourceName, entryStream);
56+
57+
var decompiler = new XamlDecompiler(bamlTypeSystem, bamlSettings) {
58+
CancellationToken = CancellationToken
59+
};
60+
var result = decompiler.Decompile(entryStream);
61+
62+
string xamlFileName;
63+
PartialTypeInfo partialTypeInfo = null;
64+
65+
var typeDefinition = result.TypeName.HasValue
66+
? bamlTypeSystem.MainModule.GetTypeDefinition(result.TypeName.Value.TopLevelTypeName)
67+
: null;
68+
if (typeDefinition != null)
69+
{
70+
xamlFileName = SanitizeFileName(typeDefinition.ReflectionName + ".xaml");
71+
partialTypeInfo = new PartialTypeInfo(typeDefinition);
72+
foreach (var member in result.GeneratedMembers)
73+
partialTypeInfo.AddDeclaredMember(member);
74+
}
75+
else
76+
{
77+
xamlFileName = Path.ChangeExtension(fileName, ".xaml");
78+
}
79+
80+
string fullPath = Path.Combine(TargetDirectory, xamlFileName);
81+
string directory = Path.GetDirectoryName(fullPath);
82+
if (!string.IsNullOrEmpty(directory))
83+
Directory.CreateDirectory(directory);
84+
result.Xaml.Save(fullPath);
85+
86+
var item = new ProjectItemInfo("Page", xamlFileName)
87+
.With("Generator", "MSBuild:Compile")
88+
.With("SubType", "Designer");
89+
if (partialTypeInfo != null)
90+
item.PartialTypes = new List<PartialTypeInfo> { partialTypeInfo };
91+
92+
return new[] { item };
93+
}
94+
}
95+
}

ICSharpCode.ILSpyCmd/ICSharpCode.ILSpyCmd.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
<ItemGroup>
5757
<ProjectReference Include="..\ICSharpCode.ILSpyX\ICSharpCode.ILSpyX.csproj" />
5858
<ProjectReference Include="..\ICSharpCode.Decompiler\ICSharpCode.Decompiler.csproj" />
59+
<ProjectReference Include="..\ICSharpCode.BamlDecompiler\ICSharpCode.BamlDecompiler.csproj" />
5960
</ItemGroup>
6061

6162
<ItemGroup>

ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Threading;
1111
using System.Threading.Tasks;
1212

13+
using ICSharpCode.BamlDecompiler;
1314
using ICSharpCode.Decompiler;
1415
using ICSharpCode.Decompiler.CSharp;
1516
using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
@@ -52,6 +53,15 @@ ilspycmd sample.dll --generate-diagrammer
5253
Generate a HTML diagrammer containing filtered type info into a custom output folder
5354
(including types in the LightJson namespace while excluding types in nested LightJson.Serialization namespace)
5455
ilspycmd sample.dll --generate-diagrammer -o c:\diagrammer --generate-diagrammer-include LightJson\\..+ --generate-diagrammer-exclude LightJson\\.Serialization\\..+
56+
57+
List all embedded resources in a WPF assembly (including BAML entries inside .g.resources containers).
58+
ilspycmd sample.dll --list-resources
59+
60+
Extract a single resource. If the name ends with .baml, the output is decompiled XAML; otherwise raw bytes.
61+
ilspycmd sample.dll --resource sample.g.resources/mainwindow.baml -o c:\decompiled
62+
63+
Decompile assembly as a compilable project and convert all BAML resources to XAML Page items.
64+
ilspycmd sample.dll -p -o c:\decompiled --decompile-baml
5565
")]
5666
[HelpOption("-h|--help")]
5767
[ProjectOptionRequiresOutputDirectoryValidation]
@@ -93,6 +103,15 @@ class ILSpyCmdProgram
93103
[Option("-l|--list <entity-type(s)>", "Lists all entities of the specified type(s). Valid types: c(lass), i(nterface), s(truct), d(elegate), e(num)", CommandOptionType.MultipleValue)]
94104
public string[] EntityTypes { get; } = Array.Empty<string>();
95105

106+
[Option("--list-resources", "Lists all embedded resources in the assembly. Entries inside .resources containers are listed individually as '<container>/<entry>'.", CommandOptionType.NoValue)]
107+
public bool ListResourcesFlag { get; }
108+
109+
[Option("--resource <name>", "Extract a single resource by name (as printed by --list-resources). Resources whose name ends with '.baml' are decompiled to XAML.", CommandOptionType.SingleValue)]
110+
public string ResourceName { get; }
111+
112+
[Option("--decompile-baml", "When used with -p, decompile BAML resources to XAML files (Page items) instead of leaving them as raw byte streams.", CommandOptionType.NoValue)]
113+
public bool DecompileBamlFlag { get; }
114+
96115
public string DecompilerVersion => "ilspycmd: " + typeof(ILSpyCmdProgram).Assembly.GetName().Version.ToString() +
97116
Environment.NewLine
98117
+ "ICSharpCode.Decompiler: " +
@@ -257,8 +276,8 @@ private async Task<int> OnExecuteAsync(CommandLineApplication app)
257276
var checkResult = await updateCheckTask;
258277
if (null != checkResult && checkResult.UpdateRecommendation)
259278
{
260-
Console.WriteLine("You are not using the latest version of the tool, please update.");
261-
Console.WriteLine($"Latest version is '{checkResult.LatestVersion}' (yours is '{checkResult.RunningVersion}')");
279+
app.Error.WriteLine("You are not using the latest version of the tool, please update.");
280+
app.Error.WriteLine($"Latest version is '{checkResult.LatestVersion}' (yours is '{checkResult.RunningVersion}')");
262281
}
263282
}
264283
}
@@ -306,6 +325,20 @@ int PerformPerFileAction(string fileName)
306325
{
307326
return DumpPackageAssemblies(fileName, outputDirectory, app);
308327
}
328+
else if (ListResourcesFlag)
329+
{
330+
if (outputDirectory != null)
331+
{
332+
string outputName = Path.GetFileNameWithoutExtension(fileName);
333+
output = File.CreateText(Path.Combine(outputDirectory, outputName) + ".resources.txt");
334+
}
335+
336+
return ListResources(fileName, output);
337+
}
338+
else if (ResourceName != null)
339+
{
340+
return ExtractResource(fileName, ResourceName, output, outputDirectory, app);
341+
}
309342
else
310343
{
311344
if (outputDirectory != null)
@@ -430,6 +463,85 @@ int ListContent(string assemblyFileName, TextWriter output, ISet<TypeKind> kinds
430463
return 0;
431464
}
432465

466+
int ListResources(string assemblyFileName, TextWriter output)
467+
{
468+
var module = new PEFile(assemblyFileName);
469+
foreach (var path in ResourceExtensions.EnumerateResourcePaths(module))
470+
{
471+
output.WriteLine(path);
472+
}
473+
return 0;
474+
}
475+
476+
int ExtractResource(string assemblyFileName, string resourceName, TextWriter output, string outputDirectory, CommandLineApplication app)
477+
{
478+
var module = new PEFile(assemblyFileName);
479+
if (!ResourceExtensions.TryGetResource(module, resourceName, out object value))
480+
{
481+
app.Error.WriteLine($"Resource '{resourceName}' not found.");
482+
app.Error.WriteLine("Available resources:");
483+
foreach (var p in ResourceExtensions.EnumerateResourcePaths(module))
484+
app.Error.WriteLine(" " + p);
485+
return ProgramExitCodes.EX_DATAERR;
486+
}
487+
488+
bool isBaml = resourceName.EndsWith(".baml", StringComparison.OrdinalIgnoreCase);
489+
if (isBaml && value is byte[] bamlBytes)
490+
{
491+
var resolver = new UniversalAssemblyResolver(assemblyFileName, false, module.Metadata.DetectTargetFrameworkId());
492+
foreach (var path in (ReferencePaths ?? Array.Empty<string>()))
493+
resolver.AddSearchDirectory(path);
494+
var bamlSettings = new BamlDecompilerSettings {
495+
ThrowOnAssemblyResolveErrors = GetSettings(module).ThrowOnAssemblyResolveErrors
496+
};
497+
498+
using var bamlStream = new MemoryStream(bamlBytes);
499+
var xaml = ResourceExtensions.DecompileBaml(module, resolver, bamlStream, bamlSettings, CancellationToken.None);
500+
if (outputDirectory != null)
501+
{
502+
string xamlFile = WholeProjectDecompiler.SanitizeFileName(Path.GetFileNameWithoutExtension(resourceName) + ".xaml");
503+
string fullPath = Path.Combine(outputDirectory, xamlFile);
504+
xaml.Save(fullPath);
505+
}
506+
else
507+
{
508+
output.Write(xaml.ToString());
509+
}
510+
return 0;
511+
}
512+
513+
if (value is byte[] binary)
514+
{
515+
if (outputDirectory != null)
516+
{
517+
string fileName = WholeProjectDecompiler.SanitizeFileName(Path.GetFileName(resourceName));
518+
string fullPath = Path.Combine(outputDirectory, fileName);
519+
File.WriteAllBytes(fullPath, binary);
520+
}
521+
else
522+
{
523+
var stdout = Console.OpenStandardOutput();
524+
stdout.Write(binary, 0, binary.Length);
525+
stdout.Flush();
526+
}
527+
}
528+
else
529+
{
530+
string text = value as string ?? value?.ToString() ?? string.Empty;
531+
if (outputDirectory != null)
532+
{
533+
string fileName = WholeProjectDecompiler.SanitizeFileName(Path.GetFileName(resourceName));
534+
string fullPath = Path.Combine(outputDirectory, fileName);
535+
File.WriteAllText(fullPath, text);
536+
}
537+
else
538+
{
539+
output.Write(text);
540+
}
541+
}
542+
return 0;
543+
}
544+
433545
int ShowIL(string assemblyFileName, TextWriter output)
434546
{
435547
var module = new PEFile(assemblyFileName);
@@ -450,7 +562,21 @@ ProjectId DecompileAsProject(string assemblyFileName, string projectFileName)
450562
{
451563
resolver.AddSearchDirectory(path);
452564
}
453-
var decompiler = new WholeProjectDecompiler(GetSettings(module), resolver, null, resolver, TryLoadPDB(module));
565+
var settings = GetSettings(module);
566+
var debugInfo = TryLoadPDB(module);
567+
WholeProjectDecompiler decompiler;
568+
if (DecompileBamlFlag)
569+
{
570+
var bamlTypeSystem = new BamlDecompilerTypeSystem(module, resolver);
571+
var bamlSettings = new BamlDecompilerSettings {
572+
ThrowOnAssemblyResolveErrors = settings.ThrowOnAssemblyResolveErrors
573+
};
574+
decompiler = new BamlAwareWholeProjectDecompiler(settings, resolver, resolver, debugInfo, bamlTypeSystem, bamlSettings);
575+
}
576+
else
577+
{
578+
decompiler = new WholeProjectDecompiler(settings, resolver, null, resolver, debugInfo);
579+
}
454580
using (var projectFileWriter = new StreamWriter(File.OpenWrite(projectFileName)))
455581
return decompiler.DecompileProject(module, Path.GetDirectoryName(projectFileName), projectFileWriter);
456582
}

0 commit comments

Comments
 (0)