Skip to content

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

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

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
Draft
Prev Previous commit
Next Next commit
Rewrote bundle object model and reading code
  • Loading branch information
ElektroKill committed Nov 5, 2023

Verified

This commit was signed with the committer’s verified signature. The key has expired.
ElektroKill ElektroKill
commit e5dc3b4f64deddbf1356b897c599fd3fa3f67524
103 changes: 0 additions & 103 deletions dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleEntry.cs

This file was deleted.

25 changes: 0 additions & 25 deletions dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleFolder.cs

This file was deleted.

168 changes: 0 additions & 168 deletions dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/SingleFileBundle.cs

This file was deleted.

44 changes: 32 additions & 12 deletions dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ You should have received a copy of the GNU General Public License
using System.IO;
using dnlib.DotNet;
using dnlib.PE;
using dnSpy.Contracts.Bundles;
using dnSpy.Contracts.Utilities;

namespace dnSpy.Contracts.Documents {
@@ -42,6 +43,8 @@ public abstract class DsDocument : IDsDocument2 {
public virtual IPEImage? PEImage => (ModuleDef as ModuleDefMD)?.Metadata?.PEImage;
/// <inheritdoc/>
public virtual SingleFileBundle? SingleFileBundle => null;
/// <inheritdoc/>
public virtual BundleEntry? BundleEntry => null;

/// <inheritdoc/>
public string Filename {
@@ -138,6 +141,9 @@ public sealed class DsPEDocument : DsDocument, IDsPEDocument, IDisposable {
public override IDsDocumentNameKey Key => FilenameKey.CreateFullPath(Filename);
/// <inheritdoc/>
public override IPEImage? PEImage { get; }
/// <inheritdoc/>
public override BundleEntry? BundleEntry => bundleEntry;
BundleEntry? bundleEntry;

/// <summary>
/// Constructor
@@ -148,6 +154,8 @@ public DsPEDocument(IPEImage peImage) {
Filename = peImage.Filename ?? string.Empty;
}

internal void SetBundleEntry(BundleEntry bundleEntry) => this.bundleEntry = bundleEntry;

/// <inheritdoc/>
public void Dispose() => PEImage!.Dispose();
}
@@ -229,6 +237,10 @@ public class DsDotNetDocument : DsDotNetDocumentBase, IDisposable {
public override DsDocumentInfo? SerializedDocument => documentInfo;
DsDocumentInfo documentInfo;

/// <inheritdoc/>
public override BundleEntry? BundleEntry => bundleEntry;
BundleEntry? bundleEntry;

/// <summary>
/// Constructor
/// </summary>
@@ -292,6 +304,8 @@ protected override TList<IDsDocument> CreateChildren() {
return list;
}

internal void SetBundleEntry(BundleEntry bundleEntry) => this.bundleEntry = bundleEntry;

/// <inheritdoc/>
public void Dispose() => ModuleDef!.Dispose();
}
@@ -327,39 +341,45 @@ public class DsBundleDocument : DsDocument, IDsPEDocument, IDisposable {
/// </summary>
public override SingleFileBundle? SingleFileBundle { get; }

readonly ModuleCreationOptions opts;
readonly string directoryOfBundle;

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

/// <inheritdoc/>
protected override TList<IDsDocument> CreateChildren() {
var list = new TList<IDsDocument>();
foreach (var entry in SingleFileBundle!.Entries) {
if (entry.Type == BundleFileType.Assembly) {
var mod = ModuleDefMD.Load(entry.Data, opts);
mod.Location = Path.Combine(directoryOfBundle, entry.RelativePath);
if (entry is AssemblyBundleEntry asmEntry) {
var mod = asmEntry.Module;
mod.Location = Path.Combine(directoryOfBundle, asmEntry.RelativePath);

var document = entry.Document = DsDotNetDocument.CreateAssembly(
DsDocumentInfo.CreateInMemory(() => (entry.Data, true), mod.Location),
mod, true);
var data = asmEntry.GetEntryData();

DsDocumentInfo documentInfo;
if (data is not null)
documentInfo = DsDocumentInfo.CreateInMemory(() => (data, true), asmEntry.FileName);
else
documentInfo = DsDocumentInfo.CreateDocument(string.Empty);

var document = DsDotNetDocument.CreateAssembly(documentInfo, mod, true);
document.SetBundleEntry(entry);
list.Add(document);
}
else if (entry.Type == BundleFileType.NativeBinary)
list.Add(entry.Document = new DsPEDocument(new PEImage(entry.Data, Path.Combine(directoryOfBundle, entry.RelativePath))));
else if (entry is NativeBinaryBundleEntry nativeEntry) {
var peDocument = new DsPEDocument(nativeEntry.PEImage);
peDocument.SetBundleEntry(entry);
list.Add(peDocument);
}
}

return list;
6 changes: 6 additions & 0 deletions dnSpy/dnSpy.Contracts.DnSpy/Documents/IDsDocument.cs
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ You should have received a copy of the GNU General Public License
using System.Linq;
using dnlib.DotNet;
using dnlib.PE;
using dnSpy.Contracts.Bundles;

namespace dnSpy.Contracts.Documents {
/// <summary>
@@ -60,6 +61,11 @@ public interface IDsDocument : IAnnotations {
/// </summary>
SingleFileBundle? SingleFileBundle { get; }

/// <summary>
/// Gets the single file bundle entry or null if it's not inside a bundle.
/// </summary>
BundleEntry? BundleEntry { get; }

/// <summary>
/// Gets/sets the filename
/// </summary>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using dnSpy.Contracts.Bundles;

namespace dnSpy.Contracts.Documents.TreeView {
/// <summary>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using dnSpy.Contracts.Bundles;

namespace dnSpy.Contracts.Documents.TreeView {
/// <summary>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using dnSpy.Contracts.Bundles;

namespace dnSpy.Contracts.Documents.TreeView {
/// <summary>
134 changes: 134 additions & 0 deletions dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System.IO;
using dnlib.DotNet;
using dnlib.PE;

namespace dnSpy.Contracts.Bundles {
/// <summary>
/// Represents one entry in a <see cref="SingleFileBundle"/>
/// </summary>
public abstract class BundleEntry {
/// <summary>
/// The type of the entry
/// </summary>
/// <seealso cref="BundleEntryType"/>
public abstract BundleEntryType Type { get; }

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

/// <summary>
/// The file name of the entry.
/// </summary>
public string FileName {
get => Path.GetFileName(RelativePath);
set => RelativePath = Path.Combine(Path.GetDirectoryName(RelativePath) ?? string.Empty, value);
}

/// <summary>
/// The parent folder
/// </summary>
public BundleFolder? ParentFolder {
get => parentFolder;
set {
if (parentFolder == value)
return;
parentFolder?.Entries.Remove(this);
value?.Entries.Add(this);
}
}
internal BundleFolder? parentFolder;

/// <summary>
/// Indicates if the entry is compressed
/// </summary>
public bool IsCompressed { get; set; }

/// <summary>
///
/// </summary>
/// <param name="relativePath"></param>
protected BundleEntry(string relativePath) => RelativePath = relativePath;
}

/// <summary>
///
/// </summary>
public abstract class UnknownBundleEntry : BundleEntry {
/// <inheritdoc/>
public override BundleEntryType Type => BundleEntryType.Unknown;

/// <summary>
///
/// </summary>
public abstract byte[] Data { get; }

/// <inheritdoc/>
protected UnknownBundleEntry(string relativePath) : base(relativePath) { }
}

/// <summary>
///
/// </summary>
public abstract class AssemblyBundleEntry : BundleEntry {
/// <inheritdoc/>
public override BundleEntryType Type => BundleEntryType.Assembly;

/// <summary>
///
/// </summary>
public abstract ModuleDefMD Module { get; }

/// <inheritdoc/>
protected AssemblyBundleEntry(string relativePath) : base(relativePath) { }
}

/// <summary>
///
/// </summary>
public abstract class NativeBinaryBundleEntry : BundleEntry {
/// <inheritdoc/>
public override BundleEntryType Type => BundleEntryType.NativeBinary;

/// <summary>
///
/// </summary>
public abstract PEImage PEImage { get; }

/// <inheritdoc/>
protected NativeBinaryBundleEntry(string relativePath) : base(relativePath) { }
}

/// <summary>
///
/// </summary>
public abstract class ConfigJSONBundleEntry : BundleEntry {
/// <inheritdoc/>
public override BundleEntryType Type { get; }

/// <summary>
///
/// </summary>
public abstract string JsonText { get; }

/// <inheritdoc/>
protected ConfigJSONBundleEntry(BundleEntryType type, string relativePath) : base(relativePath) => Type = type;
}

/// <summary>
///
/// </summary>
public abstract class SymbolBundleEntry : BundleEntry {
/// <inheritdoc/>
public override BundleEntryType Type => BundleEntryType.Symbols;

/// <summary>
///
/// </summary>
public abstract byte[] Data { get; }

/// <inheritdoc/>
protected SymbolBundleEntry(string relativePath) : base(relativePath) { }
}
}
166 changes: 166 additions & 0 deletions dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryMD.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using System.Text;
using System.Threading;
using dnlib.DotNet;
using dnlib.IO;
using dnlib.PE;

namespace dnSpy.Contracts.Bundles {
sealed class UnknownBundleEntryMD : UnknownBundleEntry {
readonly DataReaderFactory dataReaderFactory;
readonly uint offset;
readonly uint size;
readonly bool isDataCompressed;
readonly uint decompressedSize;

public override byte[] Data {
get {
if (data is null)
Interlocked.CompareExchange(ref data, ReadData(), null);
return data;
}
}
byte[]? data;

byte[] ReadData() => BundleEntryMDUtils.ReadBundleData(dataReaderFactory, offset, size, isDataCompressed, decompressedSize);

public UnknownBundleEntryMD(DataReaderFactory dataReaderFactory, uint offset, uint size, bool isCompressed, uint decompressedSize, string relativePath) : base(relativePath) {
this.dataReaderFactory = dataReaderFactory;
this.offset = offset;
this.size = size;
IsCompressed = isDataCompressed = isCompressed;
this.decompressedSize = decompressedSize;
}
}

sealed class AssemblyBundleEntryMD : AssemblyBundleEntry {
readonly DataReaderFactory dataReaderFactory;
readonly uint offset;
readonly uint size;
readonly bool isDataCompressed;
readonly uint decompressedSize;
readonly ModuleCreationOptions modCreationOptions;

public override ModuleDefMD Module {
get {
if (module is null)
Interlocked.CompareExchange(ref module, InitializeModule(), null);
return module;
}
}
ModuleDefMD? module;

ModuleDefMD InitializeModule() => ModuleDefMD.Load(Data, modCreationOptions);

internal byte[] Data {
get {
if (data is null)
Interlocked.CompareExchange(ref data, ReadData(), null);
return data;
}
}
byte[]? data;

byte[] ReadData() => BundleEntryMDUtils.ReadBundleData(dataReaderFactory, offset, size, isDataCompressed, decompressedSize);

public AssemblyBundleEntryMD(DataReaderFactory dataReaderFactory, uint offset, uint size, bool isCompressed, uint decompressedSize, ModuleCreationOptions modCreationOptions, string relativePath) : base(relativePath) {
this.dataReaderFactory = dataReaderFactory;
this.offset = offset;
this.size = size;
IsCompressed = isDataCompressed = isCompressed;
this.decompressedSize = decompressedSize;
this.modCreationOptions = modCreationOptions;
}
}

sealed class NativeBinaryBundleEntryMD : NativeBinaryBundleEntry {
readonly DataReaderFactory dataReaderFactory;
readonly uint offset;
readonly uint size;
readonly bool isDataCompressed;
readonly uint decompressedSize;

public override PEImage PEImage {
get {
if (peImage is null)
Interlocked.CompareExchange(ref peImage, InitializePEImage(), null);
return peImage;
}
}
PEImage? peImage;

PEImage InitializePEImage() => new PEImage(Data);

internal byte[] Data {
get {
if (data is null)
Interlocked.CompareExchange(ref data, ReadData(), null);
return data;
}
}
byte[]? data;

byte[] ReadData() => BundleEntryMDUtils.ReadBundleData(dataReaderFactory, offset, size, isDataCompressed, decompressedSize);

public NativeBinaryBundleEntryMD(DataReaderFactory dataReaderFactory, uint offset, uint size, bool isCompressed, uint decompressedSize, string relativePath) : base(relativePath) {
this.dataReaderFactory = dataReaderFactory;
this.offset = offset;
this.size = size;
IsCompressed = isDataCompressed = isCompressed;
this.decompressedSize = decompressedSize;
}
}

sealed class ConfigJSONBundleEntryMD : ConfigJSONBundleEntry {
readonly DataReaderFactory dataReaderFactory;
readonly uint offset;
readonly uint size;
readonly bool isDataCompressed;
readonly uint decompressedSize;

public override string JsonText {
get {
if (jsonText is null)
Interlocked.CompareExchange(ref jsonText, ReadJSONText(), null);
return jsonText;
}
}
string? jsonText;

string ReadJSONText() => Encoding.UTF8.GetString(BundleEntryMDUtils.ReadBundleData(dataReaderFactory, offset, size, isDataCompressed, decompressedSize));

public ConfigJSONBundleEntryMD(DataReaderFactory dataReaderFactory, uint offset, uint size, bool isCompressed, uint decompressedSize, BundleEntryType type, string relativePath) : base(type, relativePath) {
this.dataReaderFactory = dataReaderFactory;
this.offset = offset;
this.size = size;
IsCompressed = isDataCompressed = isCompressed;
this.decompressedSize = decompressedSize;
}
}

sealed class SymbolBundleEntryMD : SymbolBundleEntry {
readonly DataReaderFactory dataReaderFactory;
readonly uint offset;
readonly uint size;
readonly bool isDataCompressed;
readonly uint decompressedSize;

public override byte[] Data {
get {
if (data is null)
Interlocked.CompareExchange(ref data, ReadData(), null);
return data;
}
}
byte[]? data;

byte[] ReadData() => BundleEntryMDUtils.ReadBundleData(dataReaderFactory, offset, size, isDataCompressed, decompressedSize);

public SymbolBundleEntryMD(DataReaderFactory dataReaderFactory, uint offset, uint size, bool isCompressed, uint decompressedSize, string relativePath) : base(relativePath) {
this.dataReaderFactory = dataReaderFactory;
this.offset = offset;
this.size = size;
IsCompressed = isDataCompressed = isCompressed;
this.decompressedSize = decompressedSize;
}
}
}
22 changes: 22 additions & 0 deletions dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryMDUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.IO;
using System.IO.Compression;
using dnlib.IO;

namespace dnSpy.Contracts.Bundles {
static class BundleEntryMDUtils {
internal static byte[] ReadBundleData(DataReaderFactory dataReaderFactory, uint offset, uint size, bool isCompressed, uint decompressedSize) {
var reader = dataReaderFactory.CreateReader(offset, size);
if (!isCompressed)
return reader.ReadRemainingBytes();

using (var decompressedStream = new MemoryStream((int)decompressedSize)) {
using (var compressedStream = reader.AsStream()) {
using (var deflateStream = new DeflateStream(compressedStream, CompressionMode.Decompress)) {
deflateStream.CopyTo(decompressedStream);
}
}
return decompressedStream.ToArray();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
namespace dnSpy.Contracts.Documents {
namespace dnSpy.Contracts.Bundles {
/// <summary>
/// BundleFileType: Identifies the type of file embedded into the bundle.
/// BundleEntryType: 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 {
public enum BundleEntryType : byte {
/// <summary>
/// Type not determined.
/// </summary>
3 changes: 3 additions & 0 deletions dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryUser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace dnSpy.Contracts.Bundles {

}
30 changes: 30 additions & 0 deletions dnSpy/dnSpy.Contracts.Logic/Bundles/BundleExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Text;

namespace dnSpy.Contracts.Bundles {
/// <summary>
///
/// </summary>
public static class BundleExtensions {
/// <summary>
///
/// </summary>
/// <param name="entry"></param>
/// <returns></returns>
public static byte[]? GetEntryData(this BundleEntry entry) {
switch (entry) {
case AssemblyBundleEntryMD assemblyEntry:
return assemblyEntry.Data;
case ConfigJSONBundleEntry configEntry:
return Encoding.UTF8.GetBytes(configEntry.JsonText);
case NativeBinaryBundleEntryMD nativeEntry:
return nativeEntry.Data;
case SymbolBundleEntry symbolEntry:
return symbolEntry.Data;
case UnknownBundleEntry unknownEntry:
return unknownEntry.Data;
default:
return null;
}
}
}
}
85 changes: 85 additions & 0 deletions dnSpy/dnSpy.Contracts.Logic/Bundles/BundleFolder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;
using dnlib.Utils;

namespace dnSpy.Contracts.Bundles {
/// <summary>
/// Represents one folder in a <see cref="SingleFileBundle"/>
/// </summary>
public sealed class BundleFolder : IListListener<BundleEntry>, IListListener<BundleFolder> {
/// <summary>
/// Gets the relative path of the folder.
/// </summary>
public string RelativePath { get; set; }

/// <summary>
/// Gets the short name of the folder.
/// </summary>
public string Name {
get => Path.GetFileName(RelativePath);
set => RelativePath = Path.Combine(Path.GetDirectoryName(RelativePath) ?? string.Empty, value);
}

/// <summary>
/// The parent folder
/// </summary>
public BundleFolder? ParentFolder {
get => parentFolder;
set {
if (parentFolder == value)
return;
parentFolder?.NestedFolders.Remove(this);
value?.NestedFolders.Add(this);
}
}
internal BundleFolder? parentFolder;

/// <summary>
/// The folders nested within this folder.
/// </summary>
public IList<BundleFolder> NestedFolders {
get {
if (nestedFolders is null)
Interlocked.CompareExchange(ref nestedFolders, new LazyList<BundleFolder>(this), null);
return nestedFolders;
}
}
LazyList<BundleFolder>? nestedFolders;

/// <summary>
/// The entries in this folder.
/// </summary>
public IList<BundleEntry> Entries {
get {
if (entries is null)
Interlocked.CompareExchange(ref entries, new LazyList<BundleEntry>(this), null);
return entries;
}
}
LazyList<BundleEntry>? entries;

/// <summary>
/// Creates a folder with the provided relative path.
/// </summary>
public BundleFolder(string relativePath) => RelativePath = relativePath;

void IListListener<BundleEntry>.OnLazyAdd(int index, ref BundleEntry value) { }
void IListListener<BundleEntry>.OnAdd(int index, BundleEntry value) => value.parentFolder = this;
void IListListener<BundleEntry>.OnRemove(int index, BundleEntry value) => value.parentFolder = null;
void IListListener<BundleEntry>.OnResize(int index) { }
void IListListener<BundleEntry>.OnClear() {
foreach (var entry in entries!)
entry.parentFolder = null;
}

void IListListener<BundleFolder>.OnLazyAdd(int index, ref BundleFolder value) { }
void IListListener<BundleFolder>.OnAdd(int index, BundleFolder value) => value.parentFolder = this;
void IListListener<BundleFolder>.OnRemove(int index, BundleFolder value) => value.parentFolder = null;
void IListListener<BundleFolder>.OnResize(int index) { }
void IListListener<BundleFolder>.OnClear() {
foreach (var folder in nestedFolders!)
folder.parentFolder = null;
}
}
}
278 changes: 278 additions & 0 deletions dnSpy/dnSpy.Contracts.Logic/Bundles/SingleFileBundle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using dnlib.DotNet;
using dnlib.IO;
using dnlib.PE;
using dnlib.Utils;

namespace dnSpy.Contracts.Bundles {
/// <summary>
///
/// </summary>
public sealed class SingleFileBundle : IListListener<BundleEntry>, IListListener<BundleFolder> {
// 32 byte SHA-256 for ".net core bundle"
static readonly byte[] bundleSignature = {
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
};

readonly DataReaderFactory dataReaderFactory;
readonly int originalEntryCount;
readonly uint originalMajorVersion;
readonly uint entryOffset;
readonly ModuleCreationOptions moduleCreationOptions;

/// <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>
/// Bundle flags
/// Only present in version 2.0 and above.
/// </summary>
public ulong Flags { get; }

/// <summary>
/// All of the entries present in the bundle
/// </summary>
public IEnumerable<BundleEntry> Entries {
get {
for (int i = 0; i < TopLevelEntries.Count; i++)
yield return TopLevelEntries[i];

var stack = new Stack<IList<BundleFolder>>();
stack.Push(TopLevelFolders);

while (stack.Count > 0) {
var folders = stack.Pop();
for (int i = 0; i < folders.Count; i++) {
var folder = folders[i];
for (int j = 0; j < folder.Entries.Count; j++)
yield return folder.Entries[j];
stack.Push(folder.NestedFolders);
}
}
}
}

/// <summary>
/// The top level entries present in the bundle
/// </summary>
public IList<BundleEntry> TopLevelEntries {
get {
if (topLevelEntries is not null)
return topLevelEntries;
InitializeBundleEntriesAndFolder();
return topLevelEntries!;
}
}
LazyList<BundleEntry>? topLevelEntries;

/// <summary>
/// The top level folders present in the bundle.
/// </summary>
public IList<BundleFolder> TopLevelFolders {
get {
if (topLevelFolders is not null)
return topLevelFolders;
InitializeBundleEntriesAndFolder();
return topLevelFolders!;
}

}
LazyList<BundleFolder>? topLevelFolders;

SingleFileBundle(DataReaderFactory dataReaderFactory, uint headerOffset, ModuleCreationOptions moduleCreationOptions) {
this.dataReaderFactory = dataReaderFactory;
this.moduleCreationOptions = moduleCreationOptions;
var reader = dataReaderFactory.CreateReader();
reader.Position = headerOffset;
MajorVersion = originalMajorVersion = reader.ReadUInt32();
MinorVersion = reader.ReadUInt32();
EntryCount = originalEntryCount = reader.ReadInt32();
BundleID = reader.ReadSerializedString();
if (MajorVersion >= 2) {
var depsJsonOffset = reader.ReadInt64();
var depsJsonSize = reader.ReadInt64();
var runtimeConfigJsonOffset = reader.ReadInt64();
var runtimeConfigJsonSize = reader.ReadInt64();
Flags = reader.ReadUInt64();
}

entryOffset = reader.Position;
}

/// <summary>
/// Parses a bundle from the provided <see cref="IPEImage"/>
/// </summary>
/// <param name="peImage">The <see cref="IPEImage"/></param>
/// <param name="moduleCreationOptions"></param>
public static SingleFileBundle? FromPEImage(IPEImage peImage, ModuleCreationOptions moduleCreationOptions) {
if (!IsBundle(peImage, out long bundleHeaderOffset))
return null;
return new SingleFileBundle(peImage.DataReaderFactory, (uint)bundleHeaderOffset, moduleCreationOptions);
}

/// <summary>
/// Parses a bundle from the provided <see cref="IPEImage"/>
/// </summary>
/// <param name="peImage">The <see cref="IPEImage"/></param>
/// /// <param name="headerOffset"></param>
/// <param name="moduleCreationOptions"></param>
public static SingleFileBundle FromPEImage(IPEImage peImage, long headerOffset, ModuleCreationOptions moduleCreationOptions) =>
new SingleFileBundle(peImage.DataReaderFactory, (uint)headerOffset, moduleCreationOptions);

/// <summary>
/// Determines whether the provided <see cref="IPEImage"/> is a single file bundle.
/// </summary>
/// <param name="peImage">The <see cref="IPEImage"/></param>
/// <param name="bundleHeaderOffset">The offset at which a bundle header was detected</param>
public 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;
byte b = reader.ReadByte();
if (b != 0x8b)
continue;
buffer[0] = b;
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;
}

void InitializeBundleEntriesAndFolder() {
var entries = ReadBundleEntries();

var rootFolders = new LazyList<BundleFolder>(this);
var rootEntries = new LazyList<BundleEntry>(this);

var folders = new Dictionary<string, BundleFolder>();
for (int i = 0; i < entries.Length; i++) {
var entry = entries[i];
var dirName = Path.GetDirectoryName(entry.RelativePath);

if (string2.IsNullOrEmpty(dirName)) {
rootEntries.Add(entry);
continue;
}

GetFolder(dirName).Entries.Add(entry);
continue;

BundleFolder GetFolder(string directory) {
if (folders.TryGetValue(directory, out var result))
return result;
result = folders[directory] = new BundleFolder(directory);
var parentDir = Path.GetDirectoryName(directory);
if (string2.IsNullOrEmpty(parentDir))
rootFolders.Add(result);
else
GetFolder(parentDir).NestedFolders.Add(result);
return result;
}
}

Interlocked.CompareExchange(ref topLevelEntries, rootEntries, null);
Interlocked.CompareExchange(ref topLevelFolders, rootFolders, null);
}

BundleEntry[] ReadBundleEntries() {
var entries = new BundleEntry[originalEntryCount];

var reader = dataReaderFactory.CreateReader();
reader.Position = entryOffset;

bool allowCompression = originalMajorVersion >= 6;

for (int i = 0; i < originalEntryCount; i++) {
long offset = reader.ReadInt64();
long size = reader.ReadInt64();

bool isCompressed = false;
long decompressedSize = 0;
if (allowCompression) {
long compSize = reader.ReadInt64();
if (compSize != 0) {
decompressedSize = size;
size = compSize;
isCompressed = true;
}
}

var type = (BundleEntryType)reader.ReadByte();
string path = reader.ReadSerializedString();

BundleEntry entry;
switch (type) {
case BundleEntryType.Unknown:
entry = new UnknownBundleEntryMD(dataReaderFactory, (uint)offset, (uint)size, isCompressed, (uint)decompressedSize, path);
break;
case BundleEntryType.Assembly:
entry = new AssemblyBundleEntryMD(dataReaderFactory, (uint)offset, (uint)size, isCompressed, (uint)decompressedSize, moduleCreationOptions, path);
break;
case BundleEntryType.NativeBinary:
entry = new NativeBinaryBundleEntryMD(dataReaderFactory, (uint)offset, (uint)size, isCompressed, (uint)decompressedSize, path);
break;
case BundleEntryType.DepsJson:
case BundleEntryType.RuntimeConfigJson:
entry = new ConfigJSONBundleEntryMD(dataReaderFactory, (uint)offset, (uint)size, isCompressed, (uint)decompressedSize, type, path);
break;
case BundleEntryType.Symbols:
entry = new SymbolBundleEntryMD(dataReaderFactory, (uint)offset, (uint)size, isCompressed, (uint)decompressedSize, path);
break;
default:
throw new ArgumentOutOfRangeException();
}

entries[i] = entry;
}

return entries;
}

void IListListener<BundleEntry>.OnLazyAdd(int index, ref BundleEntry value) { }
void IListListener<BundleEntry>.OnAdd(int index, BundleEntry value) => value.parentFolder = null;
void IListListener<BundleEntry>.OnRemove(int index, BundleEntry value) { }
void IListListener<BundleEntry>.OnResize(int index) { }
void IListListener<BundleEntry>.OnClear() { }

void IListListener<BundleFolder>.OnLazyAdd(int index, ref BundleFolder value) { }
void IListListener<BundleFolder>.OnAdd(int index, BundleFolder value) => value.parentFolder = null;
void IListListener<BundleFolder>.OnRemove(int index, BundleFolder value) { }
void IListListener<BundleFolder>.OnResize(int index) { }
void IListListener<BundleFolder>.OnClear() { }
}
}
7 changes: 4 additions & 3 deletions dnSpy/dnSpy/Documents/DsDocumentService.cs
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@ You should have received a copy of the GNU General Public License
using System.Threading;
using dnlib.DotNet;
using dnlib.PE;
using dnSpy.Contracts.Bundles;
using dnSpy.Contracts.DnSpy.Metadata;
using dnSpy.Contracts.Documents;

@@ -459,11 +460,11 @@ IDsDocument CreateDocumentCore(DsDocumentInfo documentInfo, byte[]? fileData, st
}
}

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

return new DsPEDocument(peImage);
2 changes: 1 addition & 1 deletion dnSpy/dnSpy/Documents/Tabs/NodeDecompiler.cs
Original file line number Diff line number Diff line change
@@ -303,7 +303,7 @@ void Decompile(BundleDocumentNode node) {
decompiler.WriteCommentLine(output, ".NET Bundle:");
decompiler.WriteCommentLine(output, $"Format Version: {bundle.MajorVersion}.{bundle.MinorVersion}");
decompiler.WriteCommentLine(output, $"ID: {bundle.BundleID}");
decompiler.WriteCommentLine(output, $"Entry Count: {bundle.Entries.Count}");
decompiler.WriteCommentLine(output, $"Entry Count: {bundle.EntryCount}");
}
}

24 changes: 14 additions & 10 deletions dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using dnSpy.Contracts.Bundles;
using dnSpy.Contracts.Decompiler;
using dnSpy.Contracts.Documents;
using dnSpy.Contracts.Documents.TreeView;
@@ -20,25 +21,28 @@ public BundleDocumentNodeImpl(IDsDocument document) : base(document) { }
public override IEnumerable<TreeNodeData> CreateChildren() {
Debug2.Assert(Document.SingleFileBundle is not null);

// Ensure docuemt children are initialized.
// This is needed as loading the Children of the docment will assign the Document property of BundleEntry objects.
var _ = Document.Children;
var children = Document.Children;

foreach (var bundleFolder in Document.SingleFileBundle.TopLevelFolders) {
foreach (var bundleFolder in Document.SingleFileBundle.TopLevelFolders)
yield return new BundleFolderNodeImpl(this, bundleFolder);

var documentMap = new Dictionary<BundleEntry, IDsDocument>();
foreach (var childDocument in children) {
if (childDocument.BundleEntry is not null && childDocument.BundleEntry.ParentFolder is null)
documentMap[childDocument.BundleEntry] = childDocument;
}

foreach (var entry in Document.SingleFileBundle.TopLevelEntries) {
if (entry.Document is not null)
yield return Context.DocumentTreeView.CreateNode(this, entry.Document);
if (documentMap.TryGetValue(entry, out var document))
yield return Context.DocumentTreeView.CreateNode(this, document);
else {
switch (entry.Type) {
case BundleFileType.Unknown:
case BundleFileType.Symbols:
case BundleEntryType.Unknown:
case BundleEntryType.Symbols:
yield return new UnknownBundleEntryNodeImpl(entry);
break;
case BundleFileType.DepsJson:
case BundleFileType.RuntimeConfigJson:
case BundleEntryType.DepsJson:
case BundleEntryType.RuntimeConfigJson:
yield return new JsonBundleEntryNodeImpl(entry);
break;
default:
28 changes: 19 additions & 9 deletions dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using dnSpy.Contracts.Bundles;
using dnSpy.Contracts.Decompiler;
using dnSpy.Contracts.Documents;
using dnSpy.Contracts.Documents.TreeView;
@@ -24,21 +26,29 @@ public BundleFolderNodeImpl(BundleDocumentNode owner, BundleFolder bundleFolder)
}

public override IEnumerable<TreeNodeData> CreateChildren() {
foreach (var folder in bundleFolder.Folders) {
foreach (var folder in bundleFolder.NestedFolders) {
yield return new BundleFolderNodeImpl(owner, folder);
}

var children = owner.Document.Children;

var documentMap = new Dictionary<BundleEntry, IDsDocument>();
foreach (var childDocument in children) {
if (childDocument.BundleEntry is not null && childDocument.BundleEntry.ParentFolder == bundleFolder)
documentMap[childDocument.BundleEntry] = childDocument;
}

foreach (var entry in bundleFolder.Entries) {
if (entry.Document is not null)
yield return Context.DocumentTreeView.CreateNode(owner, entry.Document);
if (documentMap.TryGetValue(entry, out var document))
yield return Context.DocumentTreeView.CreateNode(owner, document);
else {
switch (entry.Type) {
case BundleFileType.Unknown:
case BundleFileType.Symbols:
case BundleEntryType.Unknown:
case BundleEntryType.Symbols:
yield return new UnknownBundleEntryNodeImpl(entry);
break;
case BundleFileType.DepsJson:
case BundleFileType.RuntimeConfigJson:
case BundleEntryType.DepsJson:
case BundleEntryType.RuntimeConfigJson:
yield return new JsonBundleEntryNodeImpl(entry);
break;
default:
@@ -58,9 +68,9 @@ protected override void WriteCore(ITextColorWriter output, IDecompiler decompile
output.Write(BoxedTextColor.Text, $"Entries: {bundleFolder.Entries.Count}");
}

if (bundleFolder.Folders.Count != 0) {
if (bundleFolder.NestedFolders.Count != 0) {
// TODO: localize string
output.Write(BoxedTextColor.Text, $"Subfolders: {bundleFolder.Folders.Count}");
output.Write(BoxedTextColor.Text, $"Subfolders: {bundleFolder.NestedFolders.Count}");
}
}
}
3 changes: 2 additions & 1 deletion dnSpy/dnSpy/Documents/TreeView/JsonBundleEntryNodeImpl.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Text;
using dnSpy.Contracts.Bundles;
using dnSpy.Contracts.Decompiler;
using dnSpy.Contracts.Documents;
using dnSpy.Contracts.Documents.Tabs.DocViewer;
@@ -26,7 +27,7 @@ protected override void WriteCore(ITextColorWriter output, IDecompiler decompile

public bool Decompile(IDecompileNodeContext context) {
//TODO: implement syntax highlighting
context.Output.Write(Encoding.UTF8.GetString(bundleEntry.Data), BoxedTextColor.Text);
context.Output.Write(((ConfigJSONBundleEntry)bundleEntry).JsonText, BoxedTextColor.Text);
return true;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using dnSpy.Contracts.Bundles;
using dnSpy.Contracts.Decompiler;
using dnSpy.Contracts.Documents;
using dnSpy.Contracts.Documents.TreeView;