diff --git a/ICSharpCode.Decompiler.Tests/Util/AvaloniaResourcesFileTests.cs b/ICSharpCode.Decompiler.Tests/Util/AvaloniaResourcesFileTests.cs new file mode 100644 index 000000000..f4a3da7cf --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/Util/AvaloniaResourcesFileTests.cs @@ -0,0 +1,269 @@ +// Copyright (c) 2026 Siegfried Pammer +// +// 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 System.Text; + +using ICSharpCode.Decompiler.Util; + +using NUnit.Framework; + +namespace ICSharpCode.Decompiler.Tests.Util +{ + [TestFixture] + public class AvaloniaResourcesFileTests + { + [Test] + public void ReadsEntriesWithRootedPathsAndPayloads() + { + var blob = BuildBlob( + ("/App.axaml", Encoding.UTF8.GetBytes("")), + ("Views/MainWindow.axaml", Encoding.UTF8.GetBytes("")), + ("/assets/logo.png", new byte[] { 0x89, 0x50, 0x4E, 0x47 })); + + var entries = new AvaloniaResourcesFile(new MemoryStream(blob)).ToList(); + + Assert.That(entries.Select(e => e.Key), Is.EqualTo(new[] { + "/App.axaml", + // A path without a leading slash is rooted, matching Avalonia's AssetLoader. + "/Views/MainWindow.axaml", + "/assets/logo.png", + })); + Assert.That(entries[0].Value, Is.EqualTo(Encoding.UTF8.GetBytes(""))); + Assert.That(entries[1].Value, Is.EqualTo(Encoding.UTF8.GetBytes(""))); + Assert.That(entries[2].Value, Is.EqualTo(new byte[] { 0x89, 0x50, 0x4E, 0x47 })); + } + + [Test] + public void EmptyResourceTableYieldsNoEntries() + { + var blob = BuildBlob(); + var entries = new AvaloniaResourcesFile(new MemoryStream(blob)).ToList(); + Assert.That(entries, Is.Empty); + } + + [Test] + public void SharedDataSectionIsReadPerEntry() + { + // The build task deduplicates identical files, so two paths can point at the same + // (offset, size) range in the data section. Both must read the shared bytes. + byte[] shared = Encoding.UTF8.GetBytes("dup"); + var data = new MemoryStream(); + data.Write(shared); + var index = new List<(string Path, int Offset, int Size)> { + ("/a.txt", 0, shared.Length), + ("/b.txt", 0, shared.Length), + }; + var blob = Assemble(index, data.ToArray()); + + var entries = new AvaloniaResourcesFile(new MemoryStream(blob)).ToList(); + + Assert.That(entries.Select(e => e.Key), Is.EqualTo(new[] { "/a.txt", "/b.txt" })); + Assert.That(entries[0].Value, Is.EqualTo(shared)); + Assert.That(entries[1].Value, Is.EqualTo(shared)); + } + + [Test] + public void AliasedEntriesShareOneBufferInsteadOfAmplifying() + { + // Many entries legitimately aliasing one deduplicated slice must not materialize a + // separate copy each: N entries over one D-byte slice cost O(D), not O(N * D). A crafted + // index exploits per-entry copies to amplify a small file into a huge allocation. + const int slice = 1024 * 1024; + byte[] data = new byte[slice]; + var index = Enumerable.Range(0, 500) + .Select(i => ($"/f{i}", 0, slice)) + .ToList<(string, int, int)>(); + byte[] blob = Assemble(index, data); + + long before = GC.GetAllocatedBytesForCurrentThread(); + var entries = new AvaloniaResourcesFile(new MemoryStream(blob)).ToList(); + long allocated = GC.GetAllocatedBytesForCurrentThread() - before; + + Assert.That(entries, Has.Count.EqualTo(500)); + // Per-entry copies would be 500 * 1 MB; sharing the single slice keeps it near one copy. + Assert.That(allocated, Is.LessThan(16 * 1024 * 1024), + $"parser allocated {allocated / (1024 * 1024)} MB materializing aliased entries"); + } + + [Test] + public void OverlappingDataRangesAreRejected() + { + // Honest entries reference non-overlapping regions that cannot total more than the data + // section. Distinct, overlapping ranges that sum past it are the amplification vector and + // must be rejected, not copied. + byte[] data = new byte[1000]; + var index = new List<(string Path, int Offset, int Size)> { + ("/a", 0, 1000), + ("/b", 0, 999), // distinct range overlapping the first; total 1999 > 1000 + }; + byte[] blob = Assemble(index, data); + + Assert.Throws(() => new AvaloniaResourcesFile(new MemoryStream(blob)).ToList()); + } + + [Test] + public void UnsupportedVersionThrowsBadImageFormat() + { + var ms = new MemoryStream(); + using (var bw = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true)) + { + bw.Write(0); // index length placeholder + bw.Write(1); // legacy XML index version, unsupported + bw.Write(0); // entry count + } + PatchIndexLength(ms); + + Assert.Throws(() => new AvaloniaResourcesFile(ms).ToList()); + } + + [Test] + public void HugeEntryCountDoesNotPreallocate() + { + // The entry count is attacker-controlled and must never drive the entry-array + // allocation. A tiny crafted index that claims 100 million entries has to be rejected + // as malformed without first reserving the ~1.6 GB such a count would otherwise pre-size. + var ms = new MemoryStream(); + using (var bw = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true)) + { + bw.Write(0); // index length placeholder, patched below + bw.Write(2); // BinaryCurrentVersion + bw.Write(100_000_000); // absurd entry count with no entries to back it + } + PatchIndexLength(ms); + byte[] blob = ms.ToArray(); + + long before = GC.GetAllocatedBytesForCurrentThread(); + Assert.Throws(() => new AvaloniaResourcesFile(new MemoryStream(blob)).ToList()); + long allocated = GC.GetAllocatedBytesForCurrentThread() - before; + + // A count-driven pre-allocation would be 100_000_000 * sizeof(reference pair); the parser + // must instead bound the reservation to what the few-byte index can actually hold. + Assert.That(allocated, Is.LessThan(16 * 1024 * 1024), + $"parser allocated {allocated / (1024 * 1024)} MB for a {blob.Length}-byte index; the count field drove the allocation"); + } + + [Test] + public void EntrySpillingPastIndexIntoDataSectionIsRejected() + { + // An entry whose path string starts inside the index but runs past the declared index + // end would otherwise silently read its remaining bytes (and the offset/size fields + // after it) out of the data section. The index boundary must hold for every byte of + // every entry, not just at entry starts. + var ms = new MemoryStream(); + using (var bw = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true)) + { + bw.Write(0); // index length, patched below + bw.Write(2); // BinaryCurrentVersion + bw.Write(1); // entry count + bw.Write("/a-path-longer-than-the-declared-index"); + bw.Write(0); // offset + bw.Write(0); // size + } + byte[] blob = ms.ToArray(); + // Declare the index to end in the middle of the path string; the bytes after that + // point are data-section bytes that happen to parse as the rest of the entry. + BitConverter.GetBytes(20).CopyTo(blob, 0); + + Assert.Throws(() => new AvaloniaResourcesFile(new MemoryStream(blob)).ToList()); + } + + [Test] + public void HugeClaimedPathLengthIsRejectedWithoutHugeAllocation() + { + // The 7-bit length prefix of a path string is attacker-controlled; a prefix claiming a + // ~256 MB path backed by only a few real bytes must fail as malformed without the + // claimed length driving any allocation. + var ms = new MemoryStream(); + using (var bw = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true)) + { + bw.Write(0); // index length, patched below + bw.Write(2); // BinaryCurrentVersion + bw.Write(1); // entry count + ms.Write(new byte[] { 0xFF, 0xFF, 0xFF, 0x7F }); // 7-bit-encoded length 0x0FFFFFFF + ms.Write(new byte[] { (byte)'/', (byte)'a' }); // far fewer bytes than claimed + } + PatchIndexLength(ms); + byte[] blob = ms.ToArray(); + + long before = GC.GetAllocatedBytesForCurrentThread(); + Assert.Throws(() => new AvaloniaResourcesFile(new MemoryStream(blob)).ToList()); + long allocated = GC.GetAllocatedBytesForCurrentThread() - before; + + Assert.That(allocated, Is.LessThan(1024 * 1024), + $"parser allocated {allocated} bytes for a {blob.Length}-byte blob; the claimed path length drove the allocation"); + } + + [Test] + public void OutOfBoundsEntryThrowsBadImageFormat() + { + // A crafted index whose (offset, size) runs past the data section must be rejected, + // not read out of bounds. + var index = new List<(string Path, int Offset, int Size)> { + ("/evil", 0, 1024), + }; + var blob = Assemble(index, new byte[] { 1, 2, 3 }); + + Assert.Throws(() => new AvaloniaResourcesFile(new MemoryStream(blob)).ToList()); + } + + static byte[] BuildBlob(params (string Path, byte[] Data)[] files) + { + var data = new MemoryStream(); + var index = new List<(string Path, int Offset, int Size)>(); + foreach (var (path, bytes) in files) + { + index.Add((path, (int)data.Position, bytes.Length)); + data.Write(bytes); + } + return Assemble(index, data.ToArray()); + } + + static byte[] Assemble(List<(string Path, int Offset, int Size)> index, byte[] data) + { + var ms = new MemoryStream(); + using (var bw = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true)) + { + bw.Write(0); // index length placeholder, patched below + bw.Write(2); // BinaryCurrentVersion + bw.Write(index.Count); + foreach (var (path, offset, size) in index) + { + bw.Write(path); + bw.Write(offset); + bw.Write(size); + } + } + PatchIndexLength(ms); + ms.Position = ms.Length; + ms.Write(data); + return ms.ToArray(); + } + + static void PatchIndexLength(MemoryStream ms) + { + int indexLength = (int)(ms.Length - 4); + ms.Position = 0; + using var bw = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true); + bw.Write(indexLength); + } + } +} diff --git a/ICSharpCode.Decompiler/Util/AvaloniaResourcesFile.cs b/ICSharpCode.Decompiler/Util/AvaloniaResourcesFile.cs new file mode 100644 index 000000000..e7ba7440b --- /dev/null +++ b/ICSharpCode.Decompiler/Util/AvaloniaResourcesFile.cs @@ -0,0 +1,164 @@ +#nullable enable +// Copyright (c) 2026 Siegfried Pammer +// +// 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; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace ICSharpCode.Decompiler.Util +{ + /// + /// Reader for the !AvaloniaResources manifest resource that the Avalonia UI framework + /// embeds in compiled assemblies. The blob packs every Avalonia resource (compiled XAML, + /// images, ...) behind a single index, much like a .resources file packs a managed + /// resource set. + /// + /// + /// Layout (all integers little-endian): + /// + /// Int32 indexLength: byte length of the index section that follows. + /// Index section: Int32 version (must be 2), Int32 entryCount, then per entry a + /// 7-bit length-prefixed UTF-8 path, Int32 offset and Int32 size. + /// Data section: concatenated file bytes; each entry's offset is relative to the + /// start of this section (i.e. to indexLength + 4). + /// + /// + public sealed class AvaloniaResourcesFile : IEnumerable> + { + /// + /// Name of the manifest resource that holds the packed Avalonia resources. + /// + public const string ResourceName = "!AvaloniaResources"; + + // Index format version written by Avalonia's AvaloniaResourcesIndexReaderWriter. Version 1 + // was a legacy XML index that this reader does not support. + const int BinaryCurrentVersion = 2; + + readonly List> entries = new(); + + /// + /// Returns if is the manifest + /// resource name Avalonia uses for its packed resources. + /// + public static bool IsAvaloniaResourcesEntry(string resourceName) + => resourceName == ResourceName; + + /// + /// Parses the !AvaloniaResources blob read from . + /// + /// The blob is truncated, uses an unsupported + /// index version, or contains an entry that points outside the data section. + public AvaloniaResourcesFile(Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + byte[] blob; + using (var buffer = new MemoryStream()) + { + stream.CopyTo(buffer); + blob = buffer.ToArray(); + } + + try + { + Parse(blob); + } + catch (EndOfStreamException ex) + { + throw new BadImageFormatException("Truncated !AvaloniaResources index.", ex); + } + } + + void Parse(byte[] blob) + { + using var reader = new BinaryReader(new MemoryStream(blob), Encoding.UTF8); + + int indexLength = reader.ReadInt32(); + // The data section begins right after the 4-byte length prefix and the index. + long baseOffset = 4L + indexLength; + if (indexLength < 0 || baseOffset > blob.Length) + throw new BadImageFormatException("Invalid !AvaloniaResources index length."); + + int version = reader.ReadInt32(); + if (version != BinaryCurrentVersion) + throw new BadImageFormatException($"Unsupported !AvaloniaResources index version {version}."); + + int count = reader.ReadInt32(); + if (count < 0) + throw new BadImageFormatException("Invalid !AvaloniaResources entry count."); + + // The count is attacker-controlled, so it must not be trusted as an allocation size: an + // honest entry occupies at least a 1-byte path length prefix plus two Int32s, so the index + // section cannot hold more than this many entries. A lying count is left to the per-entry + // spill guard below instead of forcing a huge pre-allocation. + const int MinEntrySize = 1 + 4 + 4; + long maxEntries = (baseOffset - reader.BaseStream.Position) / MinEntrySize; + entries.Capacity = (int)Math.Min(count, Math.Max(0, maxEntries)); + + // The build task deduplicates identical files, so many entries can legitimately alias one + // (offset, size) range in the data section; those share a single buffer here rather than + // each copying the bytes. Distinct ranges in an honest index are non-overlapping and thus + // cannot total more than the data section they occupy, so the sum of copied bytes is + // bounded by the data section length. Rejecting anything larger stops a crafted index from + // amplifying a small file into a huge allocation via overlapping, per-entry-copied slices. + long dataSectionLength = blob.Length - baseOffset; + long copiedBytes = 0; + var buffersByRange = new Dictionary(); + for (int i = 0; i < count; i++) + { + string path = GetPathRooted(reader.ReadString()); + int offset = reader.ReadInt32(); + int size = reader.ReadInt32(); + + // Every byte of every entry must come from the index section: an entry that runs + // past the declared index end would otherwise silently consume data-section bytes. + // This also rejects a count claiming more entries than the index holds. + if (reader.BaseStream.Position > baseOffset) + throw new BadImageFormatException("Malformed !AvaloniaResources index."); + + if (offset < 0 || size < 0 || baseOffset + offset + size > blob.Length) + throw new BadImageFormatException($"!AvaloniaResources entry '{path}' points outside the data section."); + + long rangeKey = ((long)offset << 32) | (uint)size; + if (!buffersByRange.TryGetValue(rangeKey, out var data)) + { + copiedBytes += size; + if (copiedBytes > dataSectionLength) + throw new BadImageFormatException("!AvaloniaResources index references more data than the data section contains."); + data = new byte[size]; + Buffer.BlockCopy(blob, (int)baseOffset + offset, data, 0, size); + buffersByRange[rangeKey] = data; + } + entries.Add(new KeyValuePair(path, data)); + } + } + + // Avalonia's AssetLoader keys resources by a path with a leading slash; a path stored + // without one is rooted on read so the keys match what Avalonia itself resolves. + static string GetPathRooted(string path) + => path.Length == 0 || path[0] != '/' ? "/" + path : path; + + public IEnumerator> GetEnumerator() => entries.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => entries.GetEnumerator(); + } +} diff --git a/ILSpy.Tests/Resources/ResourceFactoryTests.cs b/ILSpy.Tests/Resources/ResourceFactoryTests.cs index fb37ee720..08f1c89d0 100644 --- a/ILSpy.Tests/Resources/ResourceFactoryTests.cs +++ b/ILSpy.Tests/Resources/ResourceFactoryTests.cs @@ -62,6 +62,7 @@ public class ResourceFactoryTests [TestCase("favicon.ico")] [TestCase("pointer.cur")] [TestCase("strings.resources")] + [TestCase("!AvaloniaResources")] public void Typed_Resource_Names_Route_To_Specialised_Node(string name) { // Each known resource extension (.xsd/.xml/.png/.bmp/.ico/.cur/.resources/...) has a @@ -184,6 +185,57 @@ public class ResourceFactoryTests types[1].Patterns.Should().BeEquivalentTo(new[] { "*.resx" }); } + [AvaloniaTest] + public void AvaloniaResources_Unpacks_Packed_Files_Into_Child_Nodes() + { + // The !AvaloniaResources blob packs every Avalonia resource behind a single index. The + // node must unpack it into one child per packed file (sorted naturally) so each file can + // be viewed and saved on its own, mirroring how .resources files are unpacked. + + // Arrange — boot composition; build a blob with three packed files (deliberately out of + // order to exercise the natural sort) and dispatch it. + EnsureComposition(); + var node = (AvaloniaResourcesFileTreeNode)ResourceEntryNode.Create( + new ByteArrayResource("!AvaloniaResources", BuildAvaloniaResources( + ("/Views/MainWindow.axaml", Encoding.UTF8.GetBytes("")), + ("/App.axaml", Encoding.UTF8.GetBytes("")), + ("/assets/logo.png", new byte[] { 0x89, 0x50, 0x4E, 0x47 })))); + + // Act — force the lazy children to load. + node.EnsureLazyChildren(); + + // Assert — one child per packed file, keyed by the rooted path and naturally sorted. + var names = node.Children.Cast().Select(c => c.Text.ToString()).ToList(); + names.Should().Equal("/App.axaml", "/assets/logo.png", "/Views/MainWindow.axaml"); + } + + static byte[] BuildAvaloniaResources(params (string Path, byte[] Data)[] files) + { + var ms = new MemoryStream(); + var data = new MemoryStream(); + using (var bw = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true)) + { + bw.Write(0); // index length placeholder, patched below + bw.Write(2); // BinaryCurrentVersion + bw.Write(files.Length); + foreach (var (path, bytes) in files) + { + bw.Write(path); + bw.Write((int)data.Position); + bw.Write(bytes.Length); + data.Write(bytes, 0, bytes.Length); + } + } + int indexLength = (int)(ms.Length - 4); + ms.Position = 0; + using (var bw = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true)) + bw.Write(indexLength); + ms.Position = ms.Length; + data.Position = 0; + data.CopyTo(ms); + return ms.ToArray(); + } + static byte[] BuildResources((string Key, object Value)[] entries) { var ms = new MemoryStream(); diff --git a/ILSpy/TreeNodes/AvaloniaResourcesFileTreeNode.cs b/ILSpy/TreeNodes/AvaloniaResourcesFileTreeNode.cs new file mode 100644 index 000000000..b7b3adc26 --- /dev/null +++ b/ILSpy/TreeNodes/AvaloniaResourcesFileTreeNode.cs @@ -0,0 +1,74 @@ +// Copyright (c) 2026 Siegfried Pammer +// +// 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.Composition; +using System.IO; +using System.Linq; + +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.Util; +using ICSharpCode.ILSpyX.Abstractions; + +namespace ICSharpCode.ILSpy.TreeNodes +{ + [Export(typeof(IResourceNodeFactory))] + [Shared] + sealed class AvaloniaResourcesFileTreeNodeFactory : IResourceNodeFactory + { + public ITreeNode? CreateNode(Resource resource) + { + if (AvaloniaResourcesFile.IsAvaloniaResourcesEntry(resource.Name)) + return new AvaloniaResourcesFileTreeNode(resource); + return null; + } + } + + /// + /// The !AvaloniaResources manifest resource an Avalonia application embeds. It packs + /// every Avalonia resource (compiled XAML, images, ...) behind a single index; each packed + /// file is unpacked into a child that can be viewed and saved + /// individually. + /// + public sealed class AvaloniaResourcesFileTreeNode : ResourceTreeNode + { + public AvaloniaResourcesFileTreeNode(Resource r) : base(r) + { + LazyLoading = true; + } + + public override object Icon => Images.ResourceResourcesFile; + + protected override void LoadChildren() + { + // Unlike ResourcesFile, AvaloniaResourcesFile copies everything out of the stream in + // its constructor, so the stream can be disposed as soon as parsing is done. + using var s = Resource.TryOpenStream(); + if (s == null) + return; + s.Position = 0; + try + { + foreach (var entry in new AvaloniaResourcesFile(s).OrderBy(e => e.Key, NaturalStringComparer.Instance)) + Children.Add(ResourceEntryNode.Create(entry.Key, entry.Value)); + } + catch (BadImageFormatException) { /* malformed — ignore */ } + catch (EndOfStreamException) { /* truncated — ignore */ } + } + } +}