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