diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
index f361b7e61..6d00a9407 100644
--- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
+++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
@@ -88,6 +88,7 @@
+
diff --git a/ICSharpCode.Decompiler/SingleFileBundle.cs b/ICSharpCode.Decompiler/SingleFileBundle.cs
new file mode 100644
index 000000000..7d2305dd0
--- /dev/null
+++ b/ICSharpCode.Decompiler/SingleFileBundle.cs
@@ -0,0 +1,160 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.IO.MemoryMappedFiles;
+using System.Text;
+
+namespace ICSharpCode.Decompiler
+{
+ ///
+ /// Class for dealing with .NET 5 single-file bundles.
+ ///
+ /// Based on code from Microsoft.NET.HostModel.
+ ///
+ public static class SingleFileBundle
+ {
+ ///
+ /// Check if the memory-mapped data is a single-file bundle
+ ///
+ public static unsafe bool IsBundle(MemoryMappedViewAccessor view, out long bundleHeaderOffset)
+ {
+ var buffer = view.SafeMemoryMappedViewHandle;
+ byte* ptr = null;
+ buffer.AcquirePointer(ref ptr);
+ try
+ {
+ return IsBundle(ptr, checked((long)buffer.ByteLength), out bundleHeaderOffset);
+ }
+ finally
+ {
+ buffer.ReleasePointer();
+ }
+ }
+
+ public static unsafe bool IsBundle(byte* data, long size, out long bundleHeaderOffset)
+ {
+ ReadOnlySpan bundleSignature = new byte[] {
+ // 32 bytes represent the bundle signature: SHA-256 for ".net core bundle"
+ 0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
+ 0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
+ 0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
+ 0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae
+ };
+
+ byte* end = data + (size - bundleSignature.Length);
+ for (byte* ptr = data; ptr < end; ptr++)
+ {
+ if (*ptr == 0x8b && bundleSignature.SequenceEqual(new ReadOnlySpan(ptr, bundleSignature.Length)))
+ {
+ bundleHeaderOffset = *(long*)(ptr - sizeof(long));
+ return true;
+ }
+ }
+
+ bundleHeaderOffset = 0;
+ return false;
+ }
+
+ public struct Header
+ {
+ public uint MajorVersion;
+ public uint MinorVersion;
+ public int FileCount;
+ public string BundleID;
+
+ // Fields introduced with v2:
+ public long DepsJsonOffset;
+ public long DepsJsonSize;
+ public long RuntimeConfigJsonOffset;
+ public long RuntimeConfigJsonSize;
+ public ulong Flags;
+
+ public ImmutableArray Entries;
+ }
+
+ ///
+ /// FileType: Identifies the type of file embedded into the bundle.
+ ///
+ /// The bundler differentiates a few kinds of files via the manifest,
+ /// with respect to the way in which they'll be used by the runtime.
+ ///
+ public enum FileType : byte
+ {
+ Unknown, // Type not determined.
+ Assembly, // IL and R2R Assemblies
+ NativeBinary, // NativeBinaries
+ DepsJson, // .deps.json configuration file
+ RuntimeConfigJson, // .runtimeconfig.json configuration file
+ Symbols // PDB Files
+ };
+
+ public struct Entry
+ {
+ public long Offset;
+ public long Size;
+ public FileType Type;
+ public string RelativePath; // Path of an embedded file, relative to the Bundle source-directory.
+ }
+
+ static UnmanagedMemoryStream AsStream(MemoryMappedViewAccessor view)
+ {
+ long size = checked((long)view.SafeMemoryMappedViewHandle.ByteLength);
+ return new UnmanagedMemoryStream(view.SafeMemoryMappedViewHandle, 0, size);
+ }
+
+ ///
+ /// Reads the manifest header from the memory mapping.
+ ///
+ public static Header ReadManifest(MemoryMappedViewAccessor view, long bundleHeaderOffset)
+ {
+ using var stream = AsStream(view);
+ stream.Seek(bundleHeaderOffset, SeekOrigin.Begin);
+ return ReadManifest(stream);
+ }
+
+ ///
+ /// Reads the manifest header from the stream.
+ ///
+ public static Header ReadManifest(Stream stream)
+ {
+ var header = new Header();
+ using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
+ header.MajorVersion = reader.ReadUInt32();
+ header.MinorVersion = reader.ReadUInt32();
+ if (header.MajorVersion < 1 || header.MajorVersion > 2)
+ {
+ throw new InvalidDataException($"Unsupported manifest version: {header.MajorVersion}.{header.MinorVersion}");
+ }
+ header.FileCount = reader.ReadInt32();
+ header.BundleID = reader.ReadString();
+ if (header.MajorVersion >= 2)
+ {
+ header.DepsJsonOffset = reader.ReadInt64();
+ header.DepsJsonSize = reader.ReadInt64();
+ header.RuntimeConfigJsonOffset = reader.ReadInt64();
+ header.RuntimeConfigJsonSize = reader.ReadInt64();
+ header.Flags = reader.ReadUInt64();
+ }
+ var entries = ImmutableArray.CreateBuilder(header.FileCount);
+ for (int i = 0; i < header.FileCount; i++)
+ {
+ entries.Add(ReadEntry(reader));
+ }
+ header.Entries = entries.MoveToImmutable();
+ return header;
+ }
+
+ private static Entry ReadEntry(BinaryReader reader)
+ {
+ Entry entry;
+ entry.Offset = reader.ReadInt64();
+ entry.Size = reader.ReadInt64();
+ entry.Type = (FileType)reader.ReadByte();
+ entry.RelativePath = reader.ReadString();
+ return entry;
+ }
+ }
+}
diff --git a/ILSpy/LoadedAssembly.cs b/ILSpy/LoadedAssembly.cs
index 8c6d0d2e7..280ce0e8e 100644
--- a/ILSpy/LoadedAssembly.cs
+++ b/ILSpy/LoadedAssembly.cs
@@ -257,6 +257,12 @@ namespace ICSharpCode.ILSpy
{
loadAssemblyException = ex;
}
+ // If it's not a .NET module, maybe it's a single-file bundle
+ var bundle = LoadedPackage.FromBundle(fileName);
+ if (bundle != null)
+ {
+ return new LoadResult(loadAssemblyException, bundle);
+ }
// If it's not a .NET module, maybe it's a zip archive (e.g. .nupkg)
try
{
diff --git a/ILSpy/LoadedPackage.cs b/ILSpy/LoadedPackage.cs
index 077a6b432..d62be05ff 100644
--- a/ILSpy/LoadedPackage.cs
+++ b/ILSpy/LoadedPackage.cs
@@ -20,9 +20,11 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
+using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Reflection;
+using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.Metadata;
namespace ICSharpCode.ILSpy
@@ -35,6 +37,7 @@ namespace ICSharpCode.ILSpy
public enum PackageKind
{
Zip,
+ Bundle,
}
public PackageKind Kind { get; }
@@ -87,11 +90,35 @@ namespace ICSharpCode.ILSpy
public static LoadedPackage FromZipFile(string file)
{
+ Debug.WriteLine($"LoadedPackage.FromZipFile({file})");
using var archive = ZipFile.OpenRead(file);
return new LoadedPackage(PackageKind.Zip,
archive.Entries.Select(entry => new ZipFileEntry(file, entry)));
}
+ ///
+ /// Load a .NET single-file bundle.
+ ///
+ public static LoadedPackage FromBundle(string fileName)
+ {
+ using var memoryMappedFile = MemoryMappedFile.CreateFromFile(fileName, FileMode.Open, null, 0, MemoryMappedFileAccess.Read);
+ var view = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read);
+ try
+ {
+ if (!SingleFileBundle.IsBundle(view, out long bundleHeaderOffset))
+ return null;
+ var manifest = SingleFileBundle.ReadManifest(view, bundleHeaderOffset);
+ var entries = manifest.Entries.Select(e => new BundleEntry(fileName, view, e)).ToList();
+ var result = new LoadedPackage(PackageKind.Bundle, entries);
+ view = null; // don't dispose the view, we're still using it in the bundle entries
+ return result;
+ }
+ finally
+ {
+ view?.Dispose();
+ }
+ }
+
///
/// Entry inside a package folder. Effectively renames the entry.
///
@@ -140,6 +167,28 @@ namespace ICSharpCode.ILSpy
return memoryStream;
}
}
+
+ sealed class BundleEntry : PackageEntry
+ {
+ readonly string bundleFile;
+ readonly MemoryMappedViewAccessor view;
+ readonly SingleFileBundle.Entry entry;
+
+ public BundleEntry(string bundleFile, MemoryMappedViewAccessor view, SingleFileBundle.Entry entry)
+ {
+ this.bundleFile = bundleFile;
+ this.view = view;
+ this.entry = entry;
+ }
+
+ public override string Name => entry.RelativePath;
+ public override string FullName => $"bundle://{bundleFile};{Name}";
+
+ public override Stream TryOpenStream()
+ {
+ return new UnmanagedMemoryStream(view.SafeMemoryMappedViewHandle, entry.Offset, entry.Size);
+ }
+ }
}
public abstract class PackageEntry : Resource
diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs
index 1000f10b0..cb9393cfd 100644
--- a/ILSpy/TreeNodes/AssemblyTreeNode.cs
+++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs
@@ -83,7 +83,13 @@ namespace ICSharpCode.ILSpy.TreeNodes
return Images.AssemblyWarning;
var loadResult = LoadedAssembly.GetLoadResultAsync().Result;
if (loadResult.Package != null)
- return Images.NuGet;
+ {
+ return loadResult.Package.Kind switch
+ {
+ LoadedPackage.PackageKind.Zip => Images.NuGet,
+ _ => Images.Library,
+ };
+ }
return Images.Assembly;
}
else