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
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,12 @@ sealed class PEFilePETreeNodeDataProvider : PETreeNodeDataProviderBase {
: base(hexBufferService, peStructureProviderFactory, hexBufferFileServiceFactory) {
}
}

[ExportTreeNodeDataProvider(Guid = DocumentTreeViewConstants.BUNDLE_NODE_GUID)]
sealed class BundleFilePETreeNodeDataProvider : PETreeNodeDataProviderBase {
[ImportingConstructor]
BundleFilePETreeNodeDataProvider(Lazy<IHexBufferService> hexBufferService, Lazy<PEStructureProviderFactory> peStructureProviderFactory, Lazy<HexBufferFileServiceFactory> hexBufferFileServiceFactory)
: base(hexBufferService, peStructureProviderFactory, hexBufferFileServiceFactory) {
}
}
}
16 changes: 12 additions & 4 deletions Extensions/dnSpy.AsmEditor/SaveModule/SaveModuleCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ You should have received a copy of the GNU General Public License
using dnSpy.AsmEditor.Hex;
using dnSpy.AsmEditor.Properties;
using dnSpy.AsmEditor.UndoRedo;
using dnSpy.Contracts.App;
using dnSpy.Contracts.Controls;
using dnSpy.Contracts.Documents.Tabs;
using dnSpy.Contracts.Documents.TreeView;
Expand Down Expand Up @@ -87,7 +88,8 @@ sealed class SaveModuleCommand : FileMenuHandler {
this.documentSaver = documentSaver;
}

HashSet<object> GetDocuments(DocumentTreeNodeData[] nodes) {
HashSet<object> GetDocuments(DocumentTreeNodeData[] nodes, out bool hitBundle) {
hitBundle = false;
var hash = new HashSet<object>();

foreach (var node in nodes) {
Expand All @@ -100,6 +102,9 @@ HashSet<object> GetDocuments(DocumentTreeNodeData[] nodes) {
if (topNode is null || topNode.TreeNode.Parent is null)
continue;

if (fileNode.Document.SingleFileBundle is not null)
hitBundle = true;

bool added = false;

if (fileNode.Document.ModuleDef is not null) {
Expand Down Expand Up @@ -128,13 +133,16 @@ HashSet<object> GetDocuments(DocumentTreeNodeData[] nodes) {
}

public override bool IsVisible(AsmEditorContext context) => true;
public override bool IsEnabled(AsmEditorContext context) => GetDocuments(context.Nodes).Count > 0;
public override bool IsEnabled(AsmEditorContext context) => GetDocuments(context.Nodes, out _).Count > 0;

public override void Execute(AsmEditorContext context) {
var asmNodes = GetDocuments(context.Nodes);
var asmNodes = GetDocuments(context.Nodes, out bool bundle);
if (bundle)
MsgBox.Instance.Show("Warning: Entries inside bundles will not be updated!"); //TODO: localize

documentSaver.Value.Save(asmNodes);
}

public override string? GetHeader(AsmEditorContext context) => GetDocuments(context.Nodes).Count <= 1 ? dnSpy_AsmEditor_Resources.SaveModuleCommand : dnSpy_AsmEditor_Resources.SaveModulesCommand;
public override string? GetHeader(AsmEditorContext context) => GetDocuments(context.Nodes, out _).Count <= 1 ? dnSpy_AsmEditor_Resources.SaveModuleCommand : dnSpy_AsmEditor_Resources.SaveModulesCommand;
}
}
103 changes: 103 additions & 0 deletions dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Threading;
using dnlib.IO;

namespace dnSpy.Contracts.Documents {
/// <summary>
/// Represents one entry in a <see cref="SingleFileBundle"/>
/// </summary>
public sealed class BundleEntry {
byte[]? data;
DataReader reader;

/// <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 offset of the entry's data.
/// </summary>
public long Offset { get; }

/// <summary>
/// The size of the entry's data.
/// </summary>
public long Size { get; }

/// <summary>
/// The file name of the entry.
/// </summary>
public string? FileName { get; internal set; }

/// <summary>
/// Docuemnt assosciated with this entry.
/// </summary>
public IDsDocument? Document { get; internal set; }

/// <summary>
/// The raw data of the entry.
/// </summary>
public byte[] Data {
get {
if (data is not null)
return data;
Interlocked.CompareExchange(ref data, reader.ReadRemainingBytes(), null);
return data;
}
}

BundleEntry(BundleFileType type, string relativePath, long offset, long size, byte[] data) {
Type = type;
RelativePath = relativePath.Replace('/', '\\');
Offset = offset;
Size = size;
this.data = data;
}

BundleEntry(BundleFileType type, string relativePath, long offset, long size, DataReader reader) {
Type = type;
RelativePath = relativePath.Replace('/', '\\');
Offset = offset;
Size = size;
this.reader = reader;
}

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();

if (compSize == 0)
res[i] = new BundleEntry(type, path, offset, size, reader.Slice((uint)offset, (uint)size));
else
res[i] = new BundleEntry(type, path, offset, size, ReadCompressedEntryData(reader, offset, size, compSize));
}

return res;
}

static byte[] ReadCompressedEntryData(DataReader reader, long offset, long size, long compSize) {
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/Bundles/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
}
}
25 changes: 25 additions & 0 deletions dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleFolder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Collections.Generic;

namespace dnSpy.Contracts.Documents {
/// <summary>
/// Represents one folder in a <see cref="SingleFileBundle"/>
/// </summary>
public sealed class BundleFolder {
/// <summary>
/// Gets the short name of the folder.
/// </summary>
public string Name { get; }

internal BundleFolder(string name) => Name = name;

/// <summary>
/// The folders nested within this folder.
/// </summary>
public List<BundleFolder> Folders { get; } = new List<BundleFolder>();

/// <summary>
/// The entires in this folder.
/// </summary>
public List<BundleEntry> Entries { get; } = new List<BundleEntry>();
}
}
168 changes: 168 additions & 0 deletions dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/SingleFileBundle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
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>
/// All of the entries present in the bundle
/// </summary>
public IReadOnlyList<BundleEntry> Entries { get; }

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

/// <summary>
/// The top level folders present in the bundle.
/// </summary>
public IReadOnlyList<BundleFolder> TopLevelFolders { 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);

var rootFolder = new BundleFolder("");
var folders = new Dictionary<string, BundleFolder> { { "", rootFolder } };
foreach (var entry in Entries) {
(string dirname, string filename) = SeperateFileName(entry.RelativePath);
entry.FileName = filename;
GetFolder(dirname).Entries.Add(entry);
}
TopLevelEntries = rootFolder.Entries;
TopLevelFolders = rootFolder.Folders;

static (string directory, string file) SeperateFileName(string filename) {
int pos = filename.LastIndexOfAny(new[] { '/', '\\' });
return pos == -1 ? ("", filename) : (filename.Substring(0, pos), filename.Substring(pos + 1));
}

BundleFolder GetFolder(string name) {
if (folders.TryGetValue(name, out var result))
return result;
(string dirname, string basename) = SeperateFileName(name);
result = new BundleFolder(basename);
GetFolder(dirname).Folders.Add(result);
folders.Add(name, result);
return result;
}
}

/// <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;
}
}
}
Loading