diff --git a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WholeProjectDecompilerTests.cs b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WholeProjectDecompilerTests.cs
index 4deeaa779..ea54fbd6c 100644
--- a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WholeProjectDecompilerTests.cs
+++ b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WholeProjectDecompilerTests.cs
@@ -169,9 +169,41 @@ public sealed class WholeProjectDecompilerTests
}
///
- /// Emits an assembly carrying one embedded .resources container per entry in
- /// , each holding a single stream-valued entry. Streams are what
- /// makes the export write the entries out as individual files.
+ /// A rebuilt WPF project only resolves its own pack URIs when every entry of
+ /// "<AssemblyName>.g.resources" comes back under the resource ID it had before. The file on
+ /// disk cannot carry that ID - it is sanitized, and the ID is escaped - so each item pins it
+ /// with a LogicalName holding the decoded name, which is what the WPF build tasks escape again.
+ /// Entries no handler claimed have to be Resource items as well: as EmbeddedResource they would
+ /// rebuild into a manifest resource of their own instead of landing in ".g.resources".
+ ///
+ [Test]
+ public void WpfResourceEntriesCarryTheirOriginalResourceIdAsLogicalName()
+ {
+ string targetDirectory = Path.Combine(Environment.CurrentDirectory, Path.GetRandomFileName());
+ TestFriendlyProjectDecompiler decompiler = new(new UniversalAssemblyResolver(null, false, null));
+ decompiler.CaptureResources = true;
+
+ using var assembly = CreateAssemblyWithResources(
+ ("Test.g.resources", "my%20folder/window.baml"),
+ ("Test.g.resources", "resource%20test/logo.png"),
+ ("Test.resources", "plain%25folder/logo.png"));
+ decompiler.DecompileProject(new PEFile("Test.dll", assembly), targetDirectory, new StringWriter());
+ AssertDirectoryDoesntExist(targetDirectory);
+
+ Assert.That(decompiler.ResourceItems.Select(i => (i.ItemType, i.FileName, i.AdditionalProperties?["LogicalName"])),
+ Is.EquivalentTo(new[] {
+ // the build re-derives the .baml extension from the Page item type
+ ("Page", Path.Combine("my-folder", "window.xaml"), "my folder/window.xaml"),
+ ("Resource", Path.Combine("resource-test", "logo.png"), "resource test/logo.png"),
+ // not a WPF container, so the name is neither escaped nor a Resource item
+ ("EmbeddedResource", Path.Combine("plain-25folder", "logo.png"), "plain%25folder/logo.png"),
+ }));
+ }
+
+ ///
+ /// Emits an assembly carrying one embedded .resources container per distinct container name in
+ /// , each holding the stream-valued entries named for it. Streams
+ /// are what makes the export write the entries out as individual files.
///
static Stream CreateAssemblyWithResources(params (string ContainerName, string EntryName)[] resources)
{
@@ -181,14 +213,17 @@ public sealed class WholeProjectDecompilerTests
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
MemoryStream assembly = new();
- var result = compilation.Emit(assembly, manifestResources: resources.Select(r => {
- MemoryStream container = new();
- using (ResourceWriter writer = new(container))
+ var result = compilation.Emit(assembly, manifestResources: resources.GroupBy(r => r.ContainerName).Select(container => {
+ MemoryStream contents = new();
+ using (ResourceWriter writer = new(contents))
{
- writer.AddResource(r.EntryName, new MemoryStream(new byte[] { 1, 2, 3 }));
+ foreach (var entry in container)
+ {
+ writer.AddResource(entry.EntryName, new MemoryStream(new byte[] { 1, 2, 3 }));
+ }
}
- byte[] bytes = container.ToArray();
- return new ResourceDescription(r.ContainerName, () => new MemoryStream(bytes), isPublic: true);
+ byte[] bytes = contents.ToArray();
+ return new ResourceDescription(container.Key, () => new MemoryStream(bytes), isPublic: true);
}).ToArray());
Assert.That(result.Success, Is.True, () => string.Join(Environment.NewLine, result.Diagnostics));
assembly.Position = 0;
@@ -261,6 +296,8 @@ public sealed class WholeProjectDecompilerTests
public List WrittenResources { get; } = [];
+ public List ResourceItems { get; } = [];
+
// Resources are skipped unless a test asks for them, so the tests that only care about
// source files neither touch the disk nor pay for decoding them.
public bool CaptureResources { get; set; }
@@ -268,7 +305,10 @@ public sealed class WholeProjectDecompilerTests
protected override IEnumerable WriteResourceFilesInProject(MetadataFile module)
{
if (FailResourceWriting || CaptureResources)
- return base.WriteResourceFilesInProject(module);
+ {
+ ResourceItems.AddRange(base.WriteResourceFilesInProject(module));
+ return ResourceItems;
+ }
return FailResourceEnumeration
? Enumerable.Range(0, 1).Select(_ => throw new InvalidOperationException(ResourceFailure))
: [];
@@ -284,7 +324,13 @@ public sealed class WholeProjectDecompilerTests
throw new InvalidOperationException(ResourceFailure);
}
WrittenResources.Add(fileName);
- return new[] { new ProjectItemInfo("EmbeddedResource", fileName) };
+ // Stands in for the BAML resource-file handlers the real hosts plug in: a .baml entry
+ // is decompiled into a .xaml file referenced by a item, and those handlers
+ // attach no LogicalName. Everything else keeps the base class' behaviour of naming the
+ // resource entry as it is stored in the assembly.
+ return fileName.EndsWith(".baml", StringComparison.OrdinalIgnoreCase)
+ ? new[] { new ProjectItemInfo("Page", Path.ChangeExtension(fileName, ".xaml")) }
+ : new[] { new ProjectItemInfo("EmbeddedResource", fileName).With("LogicalName", resourceName) };
}
}
}
diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs
index 64eb77814..055ded316 100644
--- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs
+++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs
@@ -552,8 +552,10 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
entryStream.Position = 0;
try
{
- individualResources.AddRange(
- WriteResourceToFile(fileName, name, entryStream));
+ foreach (var item in WriteResourceToFile(fileName, name, entryStream))
+ {
+ individualResources.Add(entryNamesAreEscaped ? ToWpfProjectItem(item, name) : item);
+ }
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
@@ -640,6 +642,33 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
return new[] { new ProjectItemInfo("EmbeddedResource", fileName).With("LogicalName", resourceName) };
}
+ ///
+ /// Adjusts a project item produced from an entry of a WPF-generated ".g.resources" container
+ /// so that rebuilding the exported project puts the entry back under its original resource
+ /// ID. The file on disk cannot carry that ID - it is sanitized, and the ID is escaped - so
+ /// the item pins it with a LogicalName holding the decoded name: the WPF build tasks
+ /// lower-case and escape the LogicalName again, arriving back at the original ID. Entries
+ /// that no handler turned into some other item type are WPF Resource items; leaving them as
+ /// EmbeddedResource would rebuild them into a manifest resource of their own instead of
+ /// putting them into ".g.resources".
+ ///
+ static ProjectItemInfo ToWpfProjectItem(ProjectItemInfo item, string escapedEntryName)
+ {
+ string logicalName = Uri.UnescapeDataString(escapedEntryName);
+ if (item.FileName.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase))
+ {
+ // A decompiled BAML page is written as .xaml, and the build derives the .baml
+ // extension of the resource ID from the Page item type, not from the LogicalName.
+ logicalName = Path.ChangeExtension(logicalName, ".xaml");
+ }
+ var result = item with {
+ ItemType = item.ItemType == "EmbeddedResource" ? "Resource" : item.ItemType
+ };
+ result.AdditionalProperties ??= new Dictionary();
+ result.AdditionalProperties["LogicalName"] = logicalName;
+ return result;
+ }
+
///
/// WPF's build tasks put every Page and Resource item into "<AssemblyName>.g.resources"
/// (and into "<AssemblyName>.g.<culture>.resources" in satellite assemblies), keyed by