Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for loading/editing/saving .NET Core single file publish bundles. (#16) #49

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
Next Next commit
Unpack single file bundles and show them in the treeview
  • Loading branch information
ElektroKill committed Jun 17, 2023
commit dc9300a652a50eddb3f773a7683f042011801fd0
64 changes: 64 additions & 0 deletions dnSpy/dnSpy.Contracts.DnSpy/Documents/BundleEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using dnlib.IO;

namespace dnSpy.Contracts.Documents {
/// <summary>
/// Represents one entry in a <see cref="SingleFileBundle"/>
/// </summary>
public sealed class BundleEntry {
/// <summary>
/// Type of the entry <seealso cref="BundleFileType"/>
/// </summary>
public BundleFileType Type { get; }

/// <summary>
/// Path of an embedded file, relative to the Bundle source-directory.
/// </summary>
public string RelativePath { get; }

/// <summary>
/// The raw data of the entry.
/// </summary>
public byte[] Data { get; }

BundleEntry(BundleFileType type, string relativePath, byte[] data) {
Type = type;
RelativePath = relativePath;
Data = data;
}

internal static IReadOnlyList<BundleEntry> ReadEntries(DataReader reader, int count, bool allowCompression) {
var res = new BundleEntry[count];

for (int i = 0; i < count; i++) {
long offset = reader.ReadInt64();
long size = reader.ReadInt64();
long compSize = allowCompression ? reader.ReadInt64() : 0;
var type = (BundleFileType)reader.ReadByte();
string path = reader.ReadSerializedString();

res[i] = new BundleEntry(type, path, ReadEntryData(reader, offset, size, compSize));
}

return res;
}

static byte[] ReadEntryData(DataReader reader, long offset, long size, long compSize) {
if (compSize == 0) {
reader.Position = (uint)offset;
return reader.ReadBytes((int)size);
}

using (var decompressedStream = new MemoryStream((int)size)) {
using (var compressedStream = reader.Slice((uint)offset, (uint)compSize).AsStream()) {
using (var deflateStream = new DeflateStream(compressedStream, CompressionMode.Decompress)) {
deflateStream.CopyTo(decompressedStream);
return decompressedStream.ToArray();
}
}
}
}
}
}
39 changes: 39 additions & 0 deletions dnSpy/dnSpy.Contracts.DnSpy/Documents/BundleFileType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace dnSpy.Contracts.Documents {
/// <summary>
/// BundleFileType: 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 BundleFileType : byte {
/// <summary>
/// Type not determined.
/// </summary>
Unknown,

/// <summary>
/// IL and R2R Assemblies
/// </summary>
Assembly,

/// <summary>
/// Native Binaries
/// </summary>
NativeBinary,

/// <summary>
/// .deps.json configuration file
/// </summary>
DepsJson,

/// <summary>
/// .runtimeconfig.json configuration file
/// </summary>
RuntimeConfigJson,

/// <summary>
/// PDB Files
/// </summary>
Symbols
}
}
45 changes: 45 additions & 0 deletions dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,51 @@ protected override TList<IDsDocument> CreateChildren() {
}
}

/// <summary>
/// .NET single file bundle
/// </summary>
public class DsBundleDocument : DsDocument, IDsPEDocument, IDisposable {
/// <inheritdoc/>
public override DsDocumentInfo? SerializedDocument => DsDocumentInfo.CreateDocument(Filename);
/// <inheritdoc/>
public override IDsDocumentNameKey Key => FilenameKey.CreateFullPath(Filename);
/// <inheritdoc/>
public override IPEImage? PEImage { get; }
/// <summary>
/// The bundle represented by this document.
/// </summary>
public SingleFileBundle Bundle { get; }

ModuleCreationOptions opts;

/// <summary>
/// Constructor
/// </summary>
/// <param name="peImage">PE image</param>
/// <param name="bundle">Parsed bundle</param>
public DsBundleDocument(IPEImage peImage, SingleFileBundle bundle, ModuleCreationOptions options) {
PEImage = peImage;
Filename = peImage.Filename ?? string.Empty;
Bundle = bundle;
opts = options;
}

/// <inheritdoc/>
protected override TList<IDsDocument> CreateChildren() {
var list = new TList<IDsDocument>();
foreach (var entry in Bundle.Entries) {
if (entry.Type == BundleFileType.Assembly) {
list.Add(DsDotNetDocument.CreateAssembly(DsDocumentInfo.CreateInMemory(() => (entry.Data, true), null), ModuleDefMD.Load(entry.Data, opts), true));
}
}

return list;
}

/// <inheritdoc/>
public void Dispose() => PEImage!.Dispose();
}

/// <summary>
/// mmap'd I/O helper methods
/// </summary>
Expand Down
133 changes: 133 additions & 0 deletions dnSpy/dnSpy.Contracts.DnSpy/Documents/SingleFileBundle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using System.Collections.Generic;
using System.Linq;
using dnlib.IO;
using dnlib.PE;

namespace dnSpy.Contracts.Documents {
/// <summary>
/// Class for dealing with .NET 5 single-file bundles.
///
/// Based on code from Microsoft.NET.HostModel.
/// </summary>
public sealed class SingleFileBundle {
static readonly byte[] bundleSignature = {
// 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
};

/// <summary>
/// The major version of the bundle.
/// </summary>
public uint MajorVersion { get; }

/// <summary>
/// The minor version of the bundle.
/// </summary>
public uint MinorVersion { get; }

/// <summary>
/// Number of entries in the bundle.
/// </summary>
public int EntryCount { get; }

/// <summary>
/// ID of the bundle.
/// </summary>
public string BundleID { get; }

/// <summary>
/// Offset of the embedded *.deps.json file.
/// Only present in version 2.0 and above.
/// </summary>
public long DepsJsonOffset { get; }

/// <summary>
/// Size of the embedded *.deps.json file.
/// Only present in version 2.0 and above.
/// </summary>
public long DepsJsonSize { get; }

/// <summary>
/// Offset of the embedded *.runtimeconfig.json file.
/// Only present in version 2.0 and above.
/// </summary>
public long RuntimeConfigJsonOffset { get; }

/// <summary>
/// Size of the embedded *.runtimeconfig.json file.
/// Only present in version 2.0 and above.
/// </summary>
public long RuntimeConfigJsonSize { get; }

/// <summary>
/// Bundle flags
/// Only present in version 2.0 and above.
/// </summary>
public ulong Flags { get; }

/// <summary>
/// The entries present in the bundle
/// </summary>
public IReadOnlyList<BundleEntry> Entries { get; }

SingleFileBundle(DataReader reader, uint major, uint minor) {
MajorVersion = major;
MinorVersion = minor;
EntryCount = reader.ReadInt32();
BundleID = reader.ReadSerializedString();
if (MajorVersion >= 2) {
DepsJsonOffset = reader.ReadInt64();
DepsJsonSize = reader.ReadInt64();
RuntimeConfigJsonOffset = reader.ReadInt64();
RuntimeConfigJsonSize = reader.ReadInt64();
Flags = reader.ReadUInt64();
}

Entries = BundleEntry.ReadEntries(reader, EntryCount, MajorVersion >= 6);
}

/// <summary>
/// Parses a bundle from the provided <see cref="IPEImage"/>
/// </summary>
/// <param name="peImage">The <see cref="IPEImage"/></param>
/// <returns>The <see cref="SingleFileBundle"/> or null if its not a bundle.</returns>
public static SingleFileBundle? FromPEImage(IPEImage peImage) {
if (!IsBundle(peImage, out long bundleHeaderOffset))
return null;
var reader = peImage.CreateReader();
reader.Position = (uint)bundleHeaderOffset;
uint major = reader.ReadUInt32();
if (major < 1 || major > 6)
return null;
uint minor = reader.ReadUInt32();
return new SingleFileBundle(reader, major, minor);
}

static bool IsBundle(IPEImage peImage, out long bundleHeaderOffset) {
var reader = peImage.CreateReader();

byte[] buffer = new byte[bundleSignature.Length];
uint end = reader.Length - (uint)bundleSignature.Length;
for (uint i = 0; i < end; i++) {
reader.Position = i;
buffer[0] = reader.ReadByte();
if (buffer[0] != 0x8b)
continue;
reader.ReadBytes(buffer, 1, bundleSignature.Length - 1);
if (!buffer.SequenceEqual(bundleSignature))
continue;
reader.Position = i - sizeof(long);
bundleHeaderOffset = reader.ReadInt64();
if (bundleHeaderOffset <= 0 || bundleHeaderOffset >= reader.Length)
continue;
return true;
}

bundleHeaderOffset = 0;
return false;
}
}
}
7 changes: 7 additions & 0 deletions dnSpy/dnSpy/Documents/DsDocumentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,13 @@ IDsDocument CreateDocumentCore(DsDocumentInfo documentInfo, byte[]? fileData, st
}
}

var bundle = SingleFileBundle.FromPEImage(peImage);
if (bundle != null) {
var options = new ModuleCreationOptions(DsDotNetDocumentBase.CreateModuleContext(assemblyResolver));
options.TryToLoadPdbFromDisk = false;
return new DsBundleDocument(peImage, bundle, options);
}

return new DsPEDocument(peImage);
}
catch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ sealed class DefaultDsDocumentNodeProvider : IDsDocumentNodeProvider {
public DsDocumentNode? Create(IDocumentTreeView documentTreeView, DsDocumentNode? owner, IDsDocument document) {
if (document is IDsDotNetDocument dnDocument) {
Debug2.Assert(document.ModuleDef is not null);
if (document.AssemblyDef is null || owner is not null)
if (document.AssemblyDef is null || owner is not null && owner.Document is not DsBundleDocument)
return new ModuleDocumentNodeImpl(dnDocument);
return new AssemblyDocumentNodeImpl(dnDocument);
}
Expand Down