diff --git a/Extensions/dnSpy.AsmEditor/Hex/Nodes/PETreeNodeDataProvider.cs b/Extensions/dnSpy.AsmEditor/Hex/Nodes/PETreeNodeDataProvider.cs index f301901d9e..0f6264d9c2 100644 --- a/Extensions/dnSpy.AsmEditor/Hex/Nodes/PETreeNodeDataProvider.cs +++ b/Extensions/dnSpy.AsmEditor/Hex/Nodes/PETreeNodeDataProvider.cs @@ -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 hexBufferService, Lazy peStructureProviderFactory, Lazy hexBufferFileServiceFactory) + : base(hexBufferService, peStructureProviderFactory, hexBufferFileServiceFactory) { + } + } } diff --git a/Extensions/dnSpy.AsmEditor/SaveModule/SaveModuleCommand.cs b/Extensions/dnSpy.AsmEditor/SaveModule/SaveModuleCommand.cs index 91ceabf6e2..067bf4f0fa 100644 --- a/Extensions/dnSpy.AsmEditor/SaveModule/SaveModuleCommand.cs +++ b/Extensions/dnSpy.AsmEditor/SaveModule/SaveModuleCommand.cs @@ -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; @@ -87,7 +88,8 @@ sealed class SaveModuleCommand : FileMenuHandler { this.documentSaver = documentSaver; } - HashSet GetDocuments(DocumentTreeNodeData[] nodes) { + HashSet GetDocuments(DocumentTreeNodeData[] nodes, out bool hitBundle) { + hitBundle = false; var hash = new HashSet(); foreach (var node in nodes) { @@ -100,6 +102,9 @@ HashSet 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) { @@ -128,13 +133,16 @@ HashSet 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; } } diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs index 3187ec5053..a1716ba804 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs @@ -20,8 +20,10 @@ You should have received a copy of the GNU General Public License using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using dnlib.DotNet; using dnlib.PE; +using dnSpy.Contracts.Bundles; using dnSpy.Contracts.Utilities; namespace dnSpy.Contracts.Documents { @@ -39,6 +41,10 @@ public abstract class DsDocument : IDsDocument2 { public virtual ModuleDef? ModuleDef => null; /// public virtual IPEImage? PEImage => (ModuleDef as ModuleDefMD)?.Metadata?.PEImage; + /// + public virtual SingleFileBundle? SingleFileBundle => null; + /// + public virtual BundleEntry? BundleEntry => null; /// public string Filename { @@ -135,6 +141,9 @@ public sealed class DsPEDocument : DsDocument, IDsPEDocument, IDisposable { public override IDsDocumentNameKey Key => FilenameKey.CreateFullPath(Filename); /// public override IPEImage? PEImage { get; } + /// + public override BundleEntry? BundleEntry => bundleEntry; + BundleEntry? bundleEntry; /// /// Constructor @@ -145,6 +154,8 @@ public DsPEDocument(IPEImage peImage) { Filename = peImage.Filename ?? string.Empty; } + internal void SetBundleEntry(BundleEntry bundleEntry) => this.bundleEntry = bundleEntry; + /// public void Dispose() => PEImage!.Dispose(); } @@ -226,6 +237,10 @@ public class DsDotNetDocument : DsDotNetDocumentBase, IDisposable { public override DsDocumentInfo? SerializedDocument => documentInfo; DsDocumentInfo documentInfo; + /// + public override BundleEntry? BundleEntry => bundleEntry; + BundleEntry? bundleEntry; + /// /// Constructor /// @@ -289,6 +304,8 @@ protected override TList CreateChildren() { return list; } + internal void SetBundleEntry(BundleEntry bundleEntry) => this.bundleEntry = bundleEntry; + /// public void Dispose() => ModuleDef!.Dispose(); } @@ -309,6 +326,69 @@ protected override TList CreateChildren() { } } + /// + /// .NET single file bundle + /// + public class DsBundleDocument : DsDocument, IDsPEDocument, IDisposable { + /// + public override DsDocumentInfo? SerializedDocument => DsDocumentInfo.CreateDocument(Filename); + /// + public override IDsDocumentNameKey Key => FilenameKey.CreateFullPath(Filename); + /// + public override IPEImage? PEImage { get; } + /// + /// The bundle represented by this document. + /// + public override SingleFileBundle? SingleFileBundle { get; } + + readonly string directoryOfBundle; + + /// + /// Constructor + /// + /// PE image + /// Parsed bundle + public DsBundleDocument(IPEImage peImage, SingleFileBundle bundle) { + PEImage = peImage; + Filename = peImage.Filename ?? string.Empty; + directoryOfBundle = Path.GetDirectoryName(Filename) ?? string.Empty; + SingleFileBundle = bundle; + } + + /// + protected override TList CreateChildren() { + var list = new TList(); + foreach (var entry in SingleFileBundle!.Entries) { + if (entry is AssemblyBundleEntry asmEntry) { + var mod = asmEntry.Module; + mod.Location = Path.Combine(directoryOfBundle, asmEntry.RelativePath); + + 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 is NativeBinaryBundleEntry nativeEntry) { + var peDocument = new DsPEDocument(nativeEntry.PEImage); + peDocument.SetBundleEntry(entry); + list.Add(peDocument); + } + } + + return list; + } + + /// + public void Dispose() => PEImage!.Dispose(); + } + /// /// mmap'd I/O helper methods /// @@ -317,7 +397,7 @@ static class MemoryMappedIOHelper { /// Disable memory mapped I/O /// /// Document - public static void DisableMemoryMappedIO(IDsDocument document) { + public static void DisableMemoryMappedIO(IDsDocument? document) { if (document is null) return; DisableMemoryMappedIO(document.PEImage); diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/IDsDocument.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/IDsDocument.cs index 7165fbcaec..65dd5e5a86 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/IDsDocument.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/IDsDocument.cs @@ -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 { /// @@ -55,6 +56,16 @@ public interface IDsDocument : IAnnotations { /// IPEImage? PEImage { get; } + /// + /// Gets the single file bundle descriptor or null if it's not a single file bundle. + /// + SingleFileBundle? SingleFileBundle { get; } + + /// + /// Gets the single file bundle entry or null if it's not inside a bundle. + /// + BundleEntry? BundleEntry { get; } + /// /// Gets/sets the filename /// diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/AssemblyDocumentNode.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/AssemblyDocumentNode.cs index 6814e88f81..a4a150d897 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/AssemblyDocumentNode.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/AssemblyDocumentNode.cs @@ -19,13 +19,14 @@ You should have received a copy of the GNU General Public License using dnlib.DotNet; using dnlib.PE; +using dnSpy.Contracts.Bundles; using dnSpy.Contracts.TreeView; namespace dnSpy.Contracts.Documents.TreeView { /// /// A .NET assembly file /// - public abstract class AssemblyDocumentNode : DsDocumentNode, IMDTokenNode { + public abstract class AssemblyDocumentNode : DsDocumentNode, IMDTokenNode, IBundleEntryNode { /// /// Gets the instance /// @@ -37,6 +38,7 @@ public abstract class AssemblyDocumentNode : DsDocumentNode, IMDTokenNode { public bool IsExe => (Document.ModuleDef!.Characteristics & Characteristics.Dll) == 0; IMDTokenProvider? IMDTokenNode.Reference => Document.AssemblyDef; + BundleEntry? IBundleEntryNode.BundleEntry => Document.BundleEntry; /// /// Constructor diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/BundleDocumentNode.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/BundleDocumentNode.cs new file mode 100644 index 0000000000..4cb950ec30 --- /dev/null +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/BundleDocumentNode.cs @@ -0,0 +1,14 @@ +using System.Diagnostics; + +namespace dnSpy.Contracts.Documents.TreeView { + /// + /// A .NEt single file bundle. + /// + public abstract class BundleDocumentNode : DsDocumentNode { + /// + /// Constructor + /// + /// Document + protected BundleDocumentNode(IDsDocument document) : base(document) => Debug2.Assert(document.SingleFileBundle is not null); + } +} diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/BundleFolderNode.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/BundleFolderNode.cs new file mode 100644 index 0000000000..1e8d9225fd --- /dev/null +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/BundleFolderNode.cs @@ -0,0 +1,14 @@ +using System.Diagnostics; +using dnSpy.Contracts.Bundles; + +namespace dnSpy.Contracts.Documents.TreeView { + /// + /// Bundle folder node + /// + public abstract class BundleFolderNode : DocumentTreeNodeData { + /// + /// Constructor + /// + protected BundleFolderNode(BundleFolder bundleFolder) => Debug2.Assert(bundleFolder is not null); + } +} diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/DocumentTreeViewConstants.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/DocumentTreeViewConstants.cs index 1ff8c02f58..715ec0ad3d 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/DocumentTreeViewConstants.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/DocumentTreeViewConstants.cs @@ -45,6 +45,18 @@ public static class DocumentTreeViewConstants { /// public const string MODULE_NODE_GUID = "597B3358-A6F5-47EA-B0D2-57EDD1208333"; + /// + public const string BUNDLE_NODE_GUID = "56ADC6DE-146D-4967-AE15-1F561CB61DFC"; + + /// + public const string BUNDLE_FOLDER_NODE_GUID = "BCF6AA92-94FF-4837-9E55-0C770FCB3BB4"; + + /// + public const string BUNDLE_UNKNOWN_ENTRY_NODE_GUID = "582A8F1D-2D9E-476A-84B6-6053B983C374"; + + /// + public const string BUNDLE_JSON_ENTRY_NODE_GUID = "9C972EA7-9E52-4283-B38A-7C876A50F897"; + /// public const string RESOURCES_FOLDER_NODE_GUID = "1DD75445-9DED-482F-B6EB-4FD13E4A2197"; diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/JsonBundleEntryNode.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/JsonBundleEntryNode.cs new file mode 100644 index 0000000000..0d46eb5ba0 --- /dev/null +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/JsonBundleEntryNode.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; +using dnSpy.Contracts.Bundles; +using dnSpy.Contracts.TreeView; + +namespace dnSpy.Contracts.Documents.TreeView { + /// + /// JSON bundle entry node + /// + public abstract class JsonBundleEntryNode : DocumentTreeNodeData, IBundleEntryNode { + /// + public BundleEntry BundleEntry { get; } + + /// + /// Constructor + /// + protected JsonBundleEntryNode(BundleEntry bundleEntry) { + Debug2.Assert(bundleEntry is not null); + BundleEntry = bundleEntry; + } + } +} diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/ModuleDocumentNode.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/ModuleDocumentNode.cs index dd91cbc463..a2c0a32b55 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/ModuleDocumentNode.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/ModuleDocumentNode.cs @@ -19,19 +19,21 @@ You should have received a copy of the GNU General Public License using System.Diagnostics; using dnlib.DotNet; +using dnSpy.Contracts.Bundles; using dnSpy.Contracts.TreeView; namespace dnSpy.Contracts.Documents.TreeView { /// /// A .NET module file /// - public abstract class ModuleDocumentNode : DsDocumentNode, IMDTokenNode { + public abstract class ModuleDocumentNode : DsDocumentNode, IMDTokenNode, IBundleEntryNode { /// /// Gets the instance /// public new IDsDotNetDocument Document => (IDsDotNetDocument)base.Document; IMDTokenProvider? IMDTokenNode.Reference => Document.ModuleDef; + BundleEntry? IBundleEntryNode.BundleEntry => Document.BundleEntry; /// /// Constructor diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/PEDocumentNode.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/PEDocumentNode.cs index b0f713e5d4..8697ff49d7 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/PEDocumentNode.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/PEDocumentNode.cs @@ -19,17 +19,21 @@ You should have received a copy of the GNU General Public License using System.Diagnostics; using dnlib.PE; +using dnSpy.Contracts.Bundles; +using dnSpy.Contracts.TreeView; namespace dnSpy.Contracts.Documents.TreeView { /// /// A PE file (but not a .NET file) /// - public abstract class PEDocumentNode : DsDocumentNode { + public abstract class PEDocumentNode : DsDocumentNode, IBundleEntryNode { /// /// true if it's an .exe file, false if it's a .dll file /// public bool IsExe => (Document.PEImage!.ImageNTHeaders.FileHeader.Characteristics & Characteristics.Dll) == 0; + BundleEntry? IBundleEntryNode.BundleEntry => Document.BundleEntry; + /// /// Constructor /// diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/UnknownBundleEntryNode.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/UnknownBundleEntryNode.cs new file mode 100644 index 0000000000..6b1af0a07b --- /dev/null +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/UnknownBundleEntryNode.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; +using dnSpy.Contracts.Bundles; +using dnSpy.Contracts.TreeView; + +namespace dnSpy.Contracts.Documents.TreeView { + /// + /// Unknown bundle entry node + /// + public abstract class UnknownBundleEntryNode : DocumentTreeNodeData, IBundleEntryNode { + /// + public BundleEntry BundleEntry { get; } + + /// + /// Constructor + /// + protected UnknownBundleEntryNode(BundleEntry bundleEntry) { + Debug2.Assert(bundleEntry is not null); + BundleEntry = bundleEntry; + } + } +} diff --git a/dnSpy/dnSpy.Contracts.DnSpy/TreeView/IBundleEntryNode.cs b/dnSpy/dnSpy.Contracts.DnSpy/TreeView/IBundleEntryNode.cs new file mode 100644 index 0000000000..71f4aa1880 --- /dev/null +++ b/dnSpy/dnSpy.Contracts.DnSpy/TreeView/IBundleEntryNode.cs @@ -0,0 +1,32 @@ +/* + Copyright (C) 2023 ElektroKill + + This file is part of dnSpy + + dnSpy is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + dnSpy is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with dnSpy. If not, see . +*/ + +using dnSpy.Contracts.Bundles; + +namespace dnSpy.Contracts.TreeView { + /// + /// A node which can be a bundle entry + /// + public interface IBundleEntryNode { + /// + /// Gets the bundle entry for this node or null + /// + BundleEntry? BundleEntry { get; } + } +} diff --git a/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntry.cs b/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntry.cs new file mode 100644 index 0000000000..837ad71485 --- /dev/null +++ b/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntry.cs @@ -0,0 +1,134 @@ +using System.IO; +using dnlib.DotNet; +using dnlib.PE; + +namespace dnSpy.Contracts.Bundles { + /// + /// Represents one entry in a + /// + public abstract class BundleEntry { + /// + /// The type of the entry + /// + /// + public abstract BundleEntryType Type { get; } + + /// + /// Path of an embedded file, relative to the Bundle source-directory. + /// + public string RelativePath { get; set; } + + /// + /// The file name of the entry. + /// + public string FileName { + get => Path.GetFileName(RelativePath); + set => RelativePath = Path.Combine(Path.GetDirectoryName(RelativePath) ?? string.Empty, value); + } + + /// + /// The parent folder + /// + public BundleFolder? ParentFolder { + get => parentFolder; + set { + if (parentFolder == value) + return; + parentFolder?.Entries.Remove(this); + value?.Entries.Add(this); + } + } + internal BundleFolder? parentFolder; + + /// + /// Indicates if the entry is compressed + /// + public bool IsCompressed { get; set; } + + /// + /// + /// + /// + protected BundleEntry(string relativePath) => RelativePath = relativePath; + } + + /// + /// + /// + public abstract class UnknownBundleEntry : BundleEntry { + /// + public override BundleEntryType Type => BundleEntryType.Unknown; + + /// + /// + /// + public abstract byte[] Data { get; } + + /// + protected UnknownBundleEntry(string relativePath) : base(relativePath) { } + } + + /// + /// + /// + public abstract class AssemblyBundleEntry : BundleEntry { + /// + public override BundleEntryType Type => BundleEntryType.Assembly; + + /// + /// + /// + public abstract ModuleDefMD Module { get; } + + /// + protected AssemblyBundleEntry(string relativePath) : base(relativePath) { } + } + + /// + /// + /// + public abstract class NativeBinaryBundleEntry : BundleEntry { + /// + public override BundleEntryType Type => BundleEntryType.NativeBinary; + + /// + /// + /// + public abstract PEImage PEImage { get; } + + /// + protected NativeBinaryBundleEntry(string relativePath) : base(relativePath) { } + } + + /// + /// + /// + public abstract class ConfigJSONBundleEntry : BundleEntry { + /// + public override BundleEntryType Type { get; } + + /// + /// + /// + public abstract string JsonText { get; } + + /// + protected ConfigJSONBundleEntry(BundleEntryType type, string relativePath) : base(relativePath) => Type = type; + } + + /// + /// + /// + public abstract class SymbolBundleEntry : BundleEntry { + /// + public override BundleEntryType Type => BundleEntryType.Symbols; + + /// + /// + /// + public abstract byte[] Data { get; } + + /// + protected SymbolBundleEntry(string relativePath) : base(relativePath) { } + } +} diff --git a/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryMD.cs b/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryMD.cs new file mode 100644 index 0000000000..e32440bb94 --- /dev/null +++ b/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryMD.cs @@ -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; + } + } +} diff --git a/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryMDUtils.cs b/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryMDUtils.cs new file mode 100644 index 0000000000..0aa5df8dcd --- /dev/null +++ b/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryMDUtils.cs @@ -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(); + } + } + } +} diff --git a/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryType.cs b/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryType.cs new file mode 100644 index 0000000000..d4f7624744 --- /dev/null +++ b/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryType.cs @@ -0,0 +1,39 @@ +namespace dnSpy.Contracts.Bundles { + /// + /// 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. + /// + public enum BundleEntryType : byte { + /// + /// Type not determined. + /// + Unknown, + + /// + /// IL and R2R Assemblies + /// + Assembly, + + /// + /// Native Binaries + /// + NativeBinary, + + /// + /// .deps.json configuration file + /// + DepsJson, + + /// + /// .runtimeconfig.json configuration file + /// + RuntimeConfigJson, + + /// + /// PDB Files + /// + Symbols + } +} diff --git a/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryUser.cs b/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryUser.cs new file mode 100644 index 0000000000..22ddcf3892 --- /dev/null +++ b/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryUser.cs @@ -0,0 +1,3 @@ +namespace dnSpy.Contracts.Bundles { + +} diff --git a/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleExtensions.cs b/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleExtensions.cs new file mode 100644 index 0000000000..5f0097ce93 --- /dev/null +++ b/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleExtensions.cs @@ -0,0 +1,30 @@ +using System.Text; + +namespace dnSpy.Contracts.Bundles { + /// + /// + /// + public static class BundleExtensions { + /// + /// + /// + /// + /// + 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; + } + } + } +} diff --git a/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleFolder.cs b/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleFolder.cs new file mode 100644 index 0000000000..b14d53b0a0 --- /dev/null +++ b/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleFolder.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using dnlib.Utils; + +namespace dnSpy.Contracts.Bundles { + /// + /// Represents one folder in a + /// + public sealed class BundleFolder : IListListener, IListListener { + /// + /// Gets the relative path of the folder. + /// + public string RelativePath { get; set; } + + /// + /// Gets the short name of the folder. + /// + public string Name { + get => Path.GetFileName(RelativePath); + set => RelativePath = Path.Combine(Path.GetDirectoryName(RelativePath) ?? string.Empty, value); + } + + /// + /// The parent folder + /// + public BundleFolder? ParentFolder { + get => parentFolder; + set { + if (parentFolder == value) + return; + parentFolder?.NestedFolders.Remove(this); + value?.NestedFolders.Add(this); + } + } + internal BundleFolder? parentFolder; + + /// + /// The folders nested within this folder. + /// + public IList NestedFolders { + get { + if (nestedFolders is null) + Interlocked.CompareExchange(ref nestedFolders, new LazyList(this), null); + return nestedFolders; + } + } + LazyList? nestedFolders; + + /// + /// The entries in this folder. + /// + public IList Entries { + get { + if (entries is null) + Interlocked.CompareExchange(ref entries, new LazyList(this), null); + return entries; + } + } + LazyList? entries; + + /// + /// Creates a folder with the provided relative path. + /// + public BundleFolder(string relativePath) => RelativePath = relativePath; + + void IListListener.OnLazyAdd(int index, ref BundleEntry value) { } + void IListListener.OnAdd(int index, BundleEntry value) => value.parentFolder = this; + void IListListener.OnRemove(int index, BundleEntry value) => value.parentFolder = null; + void IListListener.OnResize(int index) { } + void IListListener.OnClear() { + foreach (var entry in entries!) + entry.parentFolder = null; + } + + void IListListener.OnLazyAdd(int index, ref BundleFolder value) { } + void IListListener.OnAdd(int index, BundleFolder value) => value.parentFolder = this; + void IListListener.OnRemove(int index, BundleFolder value) => value.parentFolder = null; + void IListListener.OnResize(int index) { } + void IListListener.OnClear() { + foreach (var folder in nestedFolders!) + folder.parentFolder = null; + } + } +} diff --git a/dnSpy/dnSpy.Contracts.Logic/Bundles/SingleFileBundle.cs b/dnSpy/dnSpy.Contracts.Logic/Bundles/SingleFileBundle.cs new file mode 100644 index 0000000000..af8b07fde9 --- /dev/null +++ b/dnSpy/dnSpy.Contracts.Logic/Bundles/SingleFileBundle.cs @@ -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 { + /// + /// + /// + public sealed class SingleFileBundle : IListListener, IListListener { + // 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; + + /// + /// The major version of the bundle. + /// + public uint MajorVersion { get; } + + /// + /// The minor version of the bundle. + /// + public uint MinorVersion { get; } + + /// + /// Number of entries in the bundle. + /// + public int EntryCount { get; } + + /// + /// ID of the bundle. + /// + public string BundleID { get; } + + /// + /// Bundle flags + /// Only present in version 2.0 and above. + /// + public ulong Flags { get; } + + /// + /// All of the entries present in the bundle + /// + public IEnumerable Entries { + get { + for (int i = 0; i < TopLevelEntries.Count; i++) + yield return TopLevelEntries[i]; + + var stack = new Stack>(); + 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); + } + } + } + } + + /// + /// The top level entries present in the bundle + /// + public IList TopLevelEntries { + get { + if (topLevelEntries is not null) + return topLevelEntries; + InitializeBundleEntriesAndFolder(); + return topLevelEntries!; + } + } + LazyList? topLevelEntries; + + /// + /// The top level folders present in the bundle. + /// + public IList TopLevelFolders { + get { + if (topLevelFolders is not null) + return topLevelFolders; + InitializeBundleEntriesAndFolder(); + return topLevelFolders!; + } + + } + LazyList? 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; + } + + /// + /// Parses a bundle from the provided + /// + /// The + /// + public static SingleFileBundle? FromPEImage(IPEImage peImage, ModuleCreationOptions moduleCreationOptions) { + if (!IsBundle(peImage, out long bundleHeaderOffset)) + return null; + return new SingleFileBundle(peImage.DataReaderFactory, (uint)bundleHeaderOffset, moduleCreationOptions); + } + + /// + /// Parses a bundle from the provided + /// + /// The + /// /// + /// + public static SingleFileBundle FromPEImage(IPEImage peImage, long headerOffset, ModuleCreationOptions moduleCreationOptions) => + new SingleFileBundle(peImage.DataReaderFactory, (uint)headerOffset, moduleCreationOptions); + + /// + /// Determines whether the provided is a single file bundle. + /// + /// The + /// The offset at which a bundle header was detected + 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(this); + var rootEntries = new LazyList(this); + + var folders = new Dictionary(); + 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.OnLazyAdd(int index, ref BundleEntry value) { } + void IListListener.OnAdd(int index, BundleEntry value) => value.parentFolder = null; + void IListListener.OnRemove(int index, BundleEntry value) { } + void IListListener.OnResize(int index) { } + void IListListener.OnClear() { } + + void IListListener.OnLazyAdd(int index, ref BundleFolder value) { } + void IListListener.OnAdd(int index, BundleFolder value) => value.parentFolder = null; + void IListListener.OnRemove(int index, BundleFolder value) { } + void IListListener.OnResize(int index) { } + void IListListener.OnClear() { } + } +} diff --git a/dnSpy/dnSpy/Documents/DsDocumentService.cs b/dnSpy/dnSpy/Documents/DsDocumentService.cs index 52d83277f7..720a30260e 100644 --- a/dnSpy/dnSpy/Documents/DsDocumentService.cs +++ b/dnSpy/dnSpy/Documents/DsDocumentService.cs @@ -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; @@ -163,6 +164,13 @@ static AssemblyNameComparerFlags ToAssemblyNameComparerFlags(FindAssemblyOptions foreach (var info in documents) { if (comparer.Equals(info.Document.AssemblyDef, assembly)) return info.Document; + + if (info.Document is DsBundleDocument) { + foreach (var documentChild in info.Document.Children) { + if (comparer.Equals(documentChild.AssemblyDef, assembly)) + return documentChild; + } + } } foreach (var info in documents) { if (info.IsAlternativeAssemblyName(assembly)) @@ -224,6 +232,13 @@ DocumentInfo Find_NoLock(IDsDocumentNameKey key) { foreach (var info in documents) { if (key.Equals(info.Document.Key)) return info; + + if (info.Document is DsBundleDocument) { + foreach (var documentChild in info.Document.Children) { + if (key.Equals(documentChild.Key)) + return new DocumentInfo(documentChild); + } + } } return default; } @@ -445,6 +460,13 @@ IDsDocument CreateDocumentCore(DsDocumentInfo documentInfo, byte[]? fileData, st } } + if (SingleFileBundle.IsBundle(peImage, out var bundleHeaderOffset)) { + var options = new ModuleCreationOptions(DsDotNetDocumentBase.CreateModuleContext(assemblyResolver)); + options.TryToLoadPdbFromDisk = false; + var bundle = SingleFileBundle.FromPEImage(peImage, bundleHeaderOffset, options); + return new DsBundleDocument(peImage, bundle); + } + return new DsPEDocument(peImage); } catch { diff --git a/dnSpy/dnSpy/Documents/Tabs/NodeDecompiler.cs b/dnSpy/dnSpy/Documents/Tabs/NodeDecompiler.cs index b2599e8e04..cb5c5bc54d 100644 --- a/dnSpy/dnSpy/Documents/Tabs/NodeDecompiler.cs +++ b/dnSpy/dnSpy/Documents/Tabs/NodeDecompiler.cs @@ -54,6 +54,7 @@ enum NodeType { ResourceElementSet, UnknownFile, Message, + BundleFile } readonly struct NodeDecompiler { @@ -171,6 +172,10 @@ public void Decompile(DocumentTreeNodeData node) { Decompile((MessageNode)node); break; + case NodeType.BundleFile: + Decompile((BundleDocumentNode)node); + break; + default: Debug.Fail($"Unknown NodeType: {nodeType}"); goto case NodeType.Unknown; @@ -276,6 +281,32 @@ void Decompile(ResourceElementSetNode node) { void Decompile(UnknownDocumentNode node) => decompiler.WriteCommentLine(output, node.Document.Filename); void Decompile(MessageNode node) => decompiler.WriteCommentLine(output, node.Message); + void Decompile(BundleDocumentNode node) { + decompiler.WriteCommentLine(output, node.Document.Filename); + var peImage = node.Document.PEImage; + if (peImage is not null) { + var timestampLine = dnSpy_Resources.Decompile_Timestamp + " "; + uint ts = peImage.ImageNTHeaders.FileHeader.TimeDateStamp; + if ((int)ts > 0) { + var date = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(ts).ToLocalTime(); + var dateString = date.ToString(CultureInfo.CurrentUICulture.DateTimeFormat); + timestampLine += $"{ts:X8} ({dateString})"; + } + else + timestampLine += $"{dnSpy_Resources.UnknownValue} ({ts:X8})"; + decompiler.WriteCommentLine(output, timestampLine); + } + var bundle = node.Document.SingleFileBundle; + if (bundle is not null) { + output.WriteLine(); + // TODO: Localize these strings. + 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.EntryCount}"); + } + } + static NodeType GetNodeType(DocumentTreeNodeData node) { NodeType nodeType; var type = node.GetType(); @@ -334,6 +365,8 @@ static NodeType GetNodeTypeSlow(DocumentTreeNodeData node) { return NodeType.UnknownFile; if (node is MessageNode) return NodeType.Message; + if (node is BundleDocumentNode) + return NodeType.BundleFile; return NodeType.Unknown; } diff --git a/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs b/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs new file mode 100644 index 0000000000..0f2ad31040 --- /dev/null +++ b/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs @@ -0,0 +1,73 @@ +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; +using dnSpy.Contracts.Images; +using dnSpy.Contracts.Text; +using dnSpy.Contracts.TreeView; +using dnSpy.Decompiler; + +namespace dnSpy.Documents.TreeView { + sealed class BundleDocumentNodeImpl : BundleDocumentNode { + public BundleDocumentNodeImpl(IDsDocument document) : base(document) { } + + public override Guid Guid => new Guid(DocumentTreeViewConstants.BUNDLE_NODE_GUID); + protected override ImageReference GetIcon(IDotNetImageService dnImgMgr) => dnImgMgr.GetImageReference(Document.PEImage!); + public override void Initialize() => TreeNode.LazyLoading = true; + + public override IEnumerable CreateChildren() { + Debug2.Assert(Document.SingleFileBundle is not null); + + var children = Document.Children; + + foreach (var bundleFolder in Document.SingleFileBundle.TopLevelFolders) + yield return new BundleFolderNodeImpl(this, bundleFolder); + + var documentMap = new Dictionary(); + 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 (documentMap.TryGetValue(entry, out var document)) + yield return Context.DocumentTreeView.CreateNode(this, document); + else { + switch (entry.Type) { + case BundleEntryType.Unknown: + case BundleEntryType.Symbols: + yield return new UnknownBundleEntryNodeImpl(entry); + break; + case BundleEntryType.DepsJson: + case BundleEntryType.RuntimeConfigJson: + yield return new JsonBundleEntryNodeImpl(entry); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + // TODO: return all bundle entries + } + + protected override void WriteCore(ITextColorWriter output, IDecompiler decompiler, DocumentNodeWriteOptions options) { + Debug2.Assert(Document.SingleFileBundle is not null); + Debug2.Assert(Document.PEImage is not null); + if ((options & DocumentNodeWriteOptions.ToolTip) == 0) + new NodeFormatter().Write(output, decompiler, Document); + else { + output.Write(BoxedTextColor.Text, TargetFrameworkUtils.GetArchString(Document.PEImage.ImageNTHeaders.FileHeader.Machine)); + + output.WriteLine(); + output.WriteFilename(Document.Filename); + } + } + + public override FilterType GetFilterType(IDocumentTreeNodeFilter filter) => + filter.GetResult(Document).FilterType; + } +} diff --git a/dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs b/dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs new file mode 100644 index 0000000000..1c772ad27e --- /dev/null +++ b/dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs @@ -0,0 +1,78 @@ +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; +using dnSpy.Contracts.Images; +using dnSpy.Contracts.Text; +using dnSpy.Contracts.TreeView; + +namespace dnSpy.Documents.TreeView { + sealed class BundleFolderNodeImpl : BundleFolderNode { + public override Guid Guid => new Guid(DocumentTreeViewConstants.BUNDLE_FOLDER_NODE_GUID); + protected override ImageReference GetIcon(IDotNetImageService dnImgMgr) => DsImages.FolderClosed; + protected override ImageReference? GetExpandedIcon(IDotNetImageService dnImgMgr) => DsImages.FolderOpened; + public override NodePathName NodePathName => new NodePathName(Guid); + public override void Initialize() => TreeNode.LazyLoading = true; + + readonly BundleFolder bundleFolder; + readonly BundleDocumentNode owner; + + public BundleFolderNodeImpl(BundleDocumentNode owner, BundleFolder bundleFolder) : base(bundleFolder) { + this.bundleFolder = bundleFolder; + this.owner = owner; + } + + public override IEnumerable CreateChildren() { + foreach (var folder in bundleFolder.NestedFolders) { + yield return new BundleFolderNodeImpl(owner, folder); + } + + var children = owner.Document.Children; + + var documentMap = new Dictionary(); + 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 (documentMap.TryGetValue(entry, out var document)) + yield return Context.DocumentTreeView.CreateNode(owner, document); + else { + switch (entry.Type) { + case BundleEntryType.Unknown: + case BundleEntryType.Symbols: + yield return new UnknownBundleEntryNodeImpl(entry); + break; + case BundleEntryType.DepsJson: + case BundleEntryType.RuntimeConfigJson: + yield return new JsonBundleEntryNodeImpl(entry); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + } + + protected override void WriteCore(ITextColorWriter output, IDecompiler decompiler, DocumentNodeWriteOptions options) { + output.Write(BoxedTextColor.Text, bundleFolder.Name); + if ((options & DocumentNodeWriteOptions.ToolTip) != 0) { + output.WriteLine(); + + if (bundleFolder.Entries.Count != 0) { + // TODO: localize string + output.Write(BoxedTextColor.Text, $"Entries: {bundleFolder.Entries.Count}"); + } + + if (bundleFolder.NestedFolders.Count != 0) { + // TODO: localize string + output.Write(BoxedTextColor.Text, $"Subfolders: {bundleFolder.NestedFolders.Count}"); + } + } + } + } +} diff --git a/dnSpy/dnSpy/Documents/TreeView/DefaultDsDocumentNodeProvider.cs b/dnSpy/dnSpy/Documents/TreeView/DefaultDsDocumentNodeProvider.cs index 9dd685bc8c..05682663e4 100644 --- a/dnSpy/dnSpy/Documents/TreeView/DefaultDsDocumentNodeProvider.cs +++ b/dnSpy/dnSpy/Documents/TreeView/DefaultDsDocumentNodeProvider.cs @@ -25,9 +25,14 @@ namespace dnSpy.Documents.TreeView { [ExportDsDocumentNodeProvider(Order = double.MaxValue)] sealed class DefaultDsDocumentNodeProvider : IDsDocumentNodeProvider { public DsDocumentNode? Create(IDocumentTreeView documentTreeView, DsDocumentNode? owner, IDsDocument document) { + if (document is DsBundleDocument bundleDocument) { + Debug2.Assert(document.SingleFileBundle is not null); + return new BundleDocumentNodeImpl(bundleDocument); + } + 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); } diff --git a/dnSpy/dnSpy/Documents/TreeView/DocumentTreeView.cs b/dnSpy/dnSpy/Documents/TreeView/DocumentTreeView.cs index 52eaa57786..b9622551df 100644 --- a/dnSpy/dnSpy/Documents/TreeView/DocumentTreeView.cs +++ b/dnSpy/dnSpy/Documents/TreeView/DocumentTreeView.cs @@ -474,6 +474,15 @@ public FieldNode Create(FieldDef field) => return n; } + // Check for bundles + foreach (var n in TopNodes.OfType()) { + n.TreeNode.EnsureChildrenLoaded(); + foreach (var a in GetAllBundleAssemblies(n)) { + if (a.Document.AssemblyDef == asm) + return a; + } + } + return null; } @@ -495,9 +504,32 @@ public FieldNode Create(FieldDef field) => return n; } + // Check for bundles + foreach (var n in TopNodes.OfType()) { + foreach (var a in GetAllBundleAssemblies(n)) { + a.TreeNode.EnsureChildrenLoaded(); + foreach (var m in a.TreeNode.DataChildren.OfType()) { + if (m.Document.ModuleDef == mod) + return m; + } + } + } + return null; } + static IEnumerable GetAllBundleAssemblies(DocumentTreeNodeData bundleNode) { + bundleNode.TreeNode.EnsureChildrenLoaded(); + foreach (var a in bundleNode.TreeNode.DataChildren.OfType()) { + yield return a; + } + foreach (var b in bundleNode.TreeNode.DataChildren.OfType()) { + b.TreeNode.EnsureChildrenLoaded(); + foreach (var a in GetAllBundleAssemblies(b)) + yield return a; + } + } + public TypeNode? FindNode(TypeDef? td) { if (td is null) return null; @@ -660,6 +692,17 @@ public IEnumerable GetAllModuleNodes() { } continue; } + + if (node is BundleDocumentNode bundleNode) { + foreach (var a in GetAllBundleAssemblies(bundleNode)) { + a.TreeNode.EnsureChildrenLoaded(); + foreach (var m in a.TreeNode.DataChildren.OfType()) { + yield return m; + } + } + + continue; + } } } diff --git a/dnSpy/dnSpy/Documents/TreeView/JsonBundleEntryNodeImpl.cs b/dnSpy/dnSpy/Documents/TreeView/JsonBundleEntryNodeImpl.cs new file mode 100644 index 0000000000..ce6506983b --- /dev/null +++ b/dnSpy/dnSpy/Documents/TreeView/JsonBundleEntryNodeImpl.cs @@ -0,0 +1,34 @@ +using System; +using System.Text; +using dnSpy.Contracts.Bundles; +using dnSpy.Contracts.Decompiler; +using dnSpy.Contracts.Documents; +using dnSpy.Contracts.Documents.Tabs.DocViewer; +using dnSpy.Contracts.Documents.TreeView; +using dnSpy.Contracts.Images; +using dnSpy.Contracts.Text; + +namespace dnSpy.Documents.TreeView { + public class JsonBundleEntryNodeImpl : JsonBundleEntryNode, IDecompileSelf { + readonly BundleEntry bundleEntry; + + public JsonBundleEntryNodeImpl(BundleEntry bundleEntry) : base(bundleEntry) => this.bundleEntry = bundleEntry; + + public override Guid Guid => new Guid(DocumentTreeViewConstants.BUNDLE_JSON_ENTRY_NODE_GUID); + + public override NodePathName NodePathName => new NodePathName(Guid); + + protected override ImageReference GetIcon(IDotNetImageService dnImgMgr) => DsImages.TextFile; + + protected override void WriteCore(ITextColorWriter output, IDecompiler decompiler, DocumentNodeWriteOptions options) { + // TODO: better tooltip + output.Write(BoxedTextColor.Text, bundleEntry.FileName); + } + + public bool Decompile(IDecompileNodeContext context) { + //TODO: implement syntax highlighting + context.Output.Write(((ConfigJSONBundleEntry)bundleEntry).JsonText, BoxedTextColor.Text); + return true; + } + } +} diff --git a/dnSpy/dnSpy/Documents/TreeView/UnknownBundleEntryNodeImpl.cs b/dnSpy/dnSpy/Documents/TreeView/UnknownBundleEntryNodeImpl.cs new file mode 100644 index 0000000000..87c16ea353 --- /dev/null +++ b/dnSpy/dnSpy/Documents/TreeView/UnknownBundleEntryNodeImpl.cs @@ -0,0 +1,26 @@ +using System; +using dnSpy.Contracts.Bundles; +using dnSpy.Contracts.Decompiler; +using dnSpy.Contracts.Documents; +using dnSpy.Contracts.Documents.TreeView; +using dnSpy.Contracts.Images; +using dnSpy.Contracts.Text; + +namespace dnSpy.Documents.TreeView { + sealed class UnknownBundleEntryNodeImpl : UnknownBundleEntryNode { + readonly BundleEntry bundleEntry; + + public UnknownBundleEntryNodeImpl(BundleEntry bundleEntry) : base(bundleEntry) { + this.bundleEntry = bundleEntry; + } + + public override Guid Guid => new Guid(DocumentTreeViewConstants.BUNDLE_UNKNOWN_ENTRY_NODE_GUID); + protected override ImageReference GetIcon(IDotNetImageService dnImgMgr) => DsImages.BinaryFile; + public override NodePathName NodePathName => new NodePathName(Guid); + + protected override void WriteCore(ITextColorWriter output, IDecompiler decompiler, DocumentNodeWriteOptions options) { + // TODO: better tooltip + output.Write(BoxedTextColor.Text, bundleEntry.FileName); + } + } +}