Browse Source

Add support for .NET bundles.

pull/2191/head
Daniel Grunwald 5 years ago
parent
commit
a354b32a16
  1. 1
      ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
  2. 160
      ICSharpCode.Decompiler/SingleFileBundle.cs
  3. 6
      ILSpy/LoadedAssembly.cs
  4. 49
      ILSpy/LoadedPackage.cs
  5. 8
      ILSpy/TreeNodes/AssemblyTreeNode.cs

1
ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj

@ -88,6 +88,7 @@
<Compile Include="IL\ControlFlow\AwaitInFinallyTransform.cs" /> <Compile Include="IL\ControlFlow\AwaitInFinallyTransform.cs" />
<Compile Include="IL\Transforms\IntroduceNativeIntTypeOnLocals.cs" /> <Compile Include="IL\Transforms\IntroduceNativeIntTypeOnLocals.cs" />
<Compile Include="Semantics\OutVarResolveResult.cs" /> <Compile Include="Semantics\OutVarResolveResult.cs" />
<Compile Include="SingleFileBundle.cs" />
<Compile Include="Solution\ProjectId.cs" /> <Compile Include="Solution\ProjectId.cs" />
<Compile Include="Solution\ProjectItem.cs" /> <Compile Include="Solution\ProjectItem.cs" />
<Compile Include="Solution\SolutionCreator.cs" /> <Compile Include="Solution\SolutionCreator.cs" />

160
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
{
/// <summary>
/// Class for dealing with .NET 5 single-file bundles.
///
/// Based on code from Microsoft.NET.HostModel.
/// </summary>
public static class SingleFileBundle
{
/// <summary>
/// Check if the memory-mapped data is a single-file bundle
/// </summary>
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<byte> 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<byte>(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<Entry> Entries;
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// Reads the manifest header from the memory mapping.
/// </summary>
public static Header ReadManifest(MemoryMappedViewAccessor view, long bundleHeaderOffset)
{
using var stream = AsStream(view);
stream.Seek(bundleHeaderOffset, SeekOrigin.Begin);
return ReadManifest(stream);
}
/// <summary>
/// Reads the manifest header from the stream.
/// </summary>
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<Entry>(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;
}
}
}

6
ILSpy/LoadedAssembly.cs

@ -257,6 +257,12 @@ namespace ICSharpCode.ILSpy
{ {
loadAssemblyException = ex; 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) // If it's not a .NET module, maybe it's a zip archive (e.g. .nupkg)
try try
{ {

49
ILSpy/LoadedPackage.cs

@ -20,9 +20,11 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.IO.MemoryMappedFiles;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Metadata;
namespace ICSharpCode.ILSpy namespace ICSharpCode.ILSpy
@ -35,6 +37,7 @@ namespace ICSharpCode.ILSpy
public enum PackageKind public enum PackageKind
{ {
Zip, Zip,
Bundle,
} }
public PackageKind Kind { get; } public PackageKind Kind { get; }
@ -87,11 +90,35 @@ namespace ICSharpCode.ILSpy
public static LoadedPackage FromZipFile(string file) public static LoadedPackage FromZipFile(string file)
{ {
Debug.WriteLine($"LoadedPackage.FromZipFile({file})");
using var archive = ZipFile.OpenRead(file); using var archive = ZipFile.OpenRead(file);
return new LoadedPackage(PackageKind.Zip, return new LoadedPackage(PackageKind.Zip,
archive.Entries.Select(entry => new ZipFileEntry(file, entry))); archive.Entries.Select(entry => new ZipFileEntry(file, entry)));
} }
/// <summary>
/// Load a .NET single-file bundle.
/// </summary>
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();
}
}
/// <summary> /// <summary>
/// Entry inside a package folder. Effectively renames the entry. /// Entry inside a package folder. Effectively renames the entry.
/// </summary> /// </summary>
@ -140,6 +167,28 @@ namespace ICSharpCode.ILSpy
return memoryStream; 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 public abstract class PackageEntry : Resource

8
ILSpy/TreeNodes/AssemblyTreeNode.cs

@ -83,7 +83,13 @@ namespace ICSharpCode.ILSpy.TreeNodes
return Images.AssemblyWarning; return Images.AssemblyWarning;
var loadResult = LoadedAssembly.GetLoadResultAsync().Result; var loadResult = LoadedAssembly.GetLoadResultAsync().Result;
if (loadResult.Package != null) if (loadResult.Package != null)
return Images.NuGet; {
return loadResult.Package.Kind switch
{
LoadedPackage.PackageKind.Zip => Images.NuGet,
_ => Images.Library,
};
}
return Images.Assembly; return Images.Assembly;
} }
else else

Loading…
Cancel
Save