From dc9300a652a50eddb3f773a7683f042011801fd0 Mon Sep 17 00:00:00 2001 From: ElektroKill Date: Sat, 31 Jul 2021 21:41:22 +0200 Subject: [PATCH 01/11] Unpack single file bundles and show them in the treeview --- .../Documents/BundleEntry.cs | 64 +++++++++ .../Documents/BundleFileType.cs | 39 +++++ .../Documents/DsDocument.cs | 45 ++++++ .../Documents/SingleFileBundle.cs | 133 ++++++++++++++++++ dnSpy/dnSpy/Documents/DsDocumentService.cs | 7 + .../TreeView/DefaultDsDocumentNodeProvider.cs | 2 +- 6 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 dnSpy/dnSpy.Contracts.DnSpy/Documents/BundleEntry.cs create mode 100644 dnSpy/dnSpy.Contracts.DnSpy/Documents/BundleFileType.cs create mode 100644 dnSpy/dnSpy.Contracts.DnSpy/Documents/SingleFileBundle.cs diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/BundleEntry.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/BundleEntry.cs new file mode 100644 index 0000000000..19ca6d2dcc --- /dev/null +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/BundleEntry.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using dnlib.IO; + +namespace dnSpy.Contracts.Documents { + /// + /// Represents one entry in a + /// + public sealed class BundleEntry { + /// + /// Type of the entry + /// + public BundleFileType Type { get; } + + /// + /// Path of an embedded file, relative to the Bundle source-directory. + /// + public string RelativePath { get; } + + /// + /// The raw data of the entry. + /// + public byte[] Data { get; } + + BundleEntry(BundleFileType type, string relativePath, byte[] data) { + Type = type; + RelativePath = relativePath; + Data = data; + } + + internal static IReadOnlyList ReadEntries(DataReader reader, int count, bool allowCompression) { + var res = new BundleEntry[count]; + + for (int i = 0; i < count; i++) { + long offset = reader.ReadInt64(); + long size = reader.ReadInt64(); + long compSize = allowCompression ? reader.ReadInt64() : 0; + var type = (BundleFileType)reader.ReadByte(); + string path = reader.ReadSerializedString(); + + res[i] = new BundleEntry(type, path, ReadEntryData(reader, offset, size, compSize)); + } + + return res; + } + + static byte[] ReadEntryData(DataReader reader, long offset, long size, long compSize) { + if (compSize == 0) { + reader.Position = (uint)offset; + return reader.ReadBytes((int)size); + } + + using (var decompressedStream = new MemoryStream((int)size)) { + using (var compressedStream = reader.Slice((uint)offset, (uint)compSize).AsStream()) { + using (var deflateStream = new DeflateStream(compressedStream, CompressionMode.Decompress)) { + deflateStream.CopyTo(decompressedStream); + return decompressedStream.ToArray(); + } + } + } + } + } +} diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/BundleFileType.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/BundleFileType.cs new file mode 100644 index 0000000000..4f49c23da8 --- /dev/null +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/BundleFileType.cs @@ -0,0 +1,39 @@ +namespace dnSpy.Contracts.Documents { + /// + /// 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. + /// + public enum BundleFileType : 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.DnSpy/Documents/DsDocument.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs index 3187ec5053..deec0721ca 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs @@ -309,6 +309,51 @@ 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 SingleFileBundle Bundle { get; } + + ModuleCreationOptions opts; + + /// + /// Constructor + /// + /// PE image + /// Parsed bundle + public DsBundleDocument(IPEImage peImage, SingleFileBundle bundle, ModuleCreationOptions options) { + PEImage = peImage; + Filename = peImage.Filename ?? string.Empty; + Bundle = bundle; + opts = options; + } + + /// + protected override TList CreateChildren() { + var list = new TList(); + foreach (var entry in Bundle.Entries) { + if (entry.Type == BundleFileType.Assembly) { + list.Add(DsDotNetDocument.CreateAssembly(DsDocumentInfo.CreateInMemory(() => (entry.Data, true), null), ModuleDefMD.Load(entry.Data, opts), true)); + } + } + + return list; + } + + /// + public void Dispose() => PEImage!.Dispose(); + } + /// /// mmap'd I/O helper methods /// diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/SingleFileBundle.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/SingleFileBundle.cs new file mode 100644 index 0000000000..1c04230d3c --- /dev/null +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/SingleFileBundle.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using System.Linq; +using dnlib.IO; +using dnlib.PE; + +namespace dnSpy.Contracts.Documents { + /// + /// Class for dealing with .NET 5 single-file bundles. + /// + /// Based on code from Microsoft.NET.HostModel. + /// + 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 + }; + + /// + /// 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; } + + /// + /// Offset of the embedded *.deps.json file. + /// Only present in version 2.0 and above. + /// + public long DepsJsonOffset { get; } + + /// + /// Size of the embedded *.deps.json file. + /// Only present in version 2.0 and above. + /// + public long DepsJsonSize { get; } + + /// + /// Offset of the embedded *.runtimeconfig.json file. + /// Only present in version 2.0 and above. + /// + public long RuntimeConfigJsonOffset { get; } + + /// + /// Size of the embedded *.runtimeconfig.json file. + /// Only present in version 2.0 and above. + /// + public long RuntimeConfigJsonSize { get; } + + /// + /// Bundle flags + /// Only present in version 2.0 and above. + /// + public ulong Flags { get; } + + /// + /// The entries present in the bundle + /// + public IReadOnlyList Entries { get; } + + SingleFileBundle(DataReader reader, uint major, uint minor) { + MajorVersion = major; + MinorVersion = minor; + EntryCount = reader.ReadInt32(); + BundleID = reader.ReadSerializedString(); + if (MajorVersion >= 2) { + DepsJsonOffset = reader.ReadInt64(); + DepsJsonSize = reader.ReadInt64(); + RuntimeConfigJsonOffset = reader.ReadInt64(); + RuntimeConfigJsonSize = reader.ReadInt64(); + Flags = reader.ReadUInt64(); + } + + Entries = BundleEntry.ReadEntries(reader, EntryCount, MajorVersion >= 6); + } + + /// + /// Parses a bundle from the provided + /// + /// The + /// The or null if its not a bundle. + 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; + } + } +} diff --git a/dnSpy/dnSpy/Documents/DsDocumentService.cs b/dnSpy/dnSpy/Documents/DsDocumentService.cs index 52d83277f7..ace44b01f1 100644 --- a/dnSpy/dnSpy/Documents/DsDocumentService.cs +++ b/dnSpy/dnSpy/Documents/DsDocumentService.cs @@ -445,6 +445,13 @@ IDsDocument CreateDocumentCore(DsDocumentInfo documentInfo, byte[]? fileData, st } } + var bundle = SingleFileBundle.FromPEImage(peImage); + if (bundle != null) { + var options = new ModuleCreationOptions(DsDotNetDocumentBase.CreateModuleContext(assemblyResolver)); + options.TryToLoadPdbFromDisk = false; + return new DsBundleDocument(peImage, bundle, options); + } + return new DsPEDocument(peImage); } catch { diff --git a/dnSpy/dnSpy/Documents/TreeView/DefaultDsDocumentNodeProvider.cs b/dnSpy/dnSpy/Documents/TreeView/DefaultDsDocumentNodeProvider.cs index 9dd685bc8c..8ccaf8094d 100644 --- a/dnSpy/dnSpy/Documents/TreeView/DefaultDsDocumentNodeProvider.cs +++ b/dnSpy/dnSpy/Documents/TreeView/DefaultDsDocumentNodeProvider.cs @@ -27,7 +27,7 @@ sealed class DefaultDsDocumentNodeProvider : IDsDocumentNodeProvider { public DsDocumentNode? Create(IDocumentTreeView documentTreeView, DsDocumentNode? owner, IDsDocument document) { if (document is IDsDotNetDocument dnDocument) { Debug2.Assert(document.ModuleDef is not null); - if (document.AssemblyDef is null || owner is not null) + if (document.AssemblyDef is null || owner is not null && owner.Document is not DsBundleDocument) return new ModuleDocumentNodeImpl(dnDocument); return new AssemblyDocumentNodeImpl(dnDocument); } From 2383c357e121a2273910561f54a369cfdf2a8506 Mon Sep 17 00:00:00 2001 From: ElektroKill Date: Sun, 14 Nov 2021 21:08:36 +0100 Subject: [PATCH 02/11] Further work on single file bundle implementation --- .../Hex/Nodes/PETreeNodeDataProvider.cs | 8 ++++ .../Documents/DsDocument.cs | 11 +++-- .../Documents/IDsDocument.cs | 5 +++ .../Documents/TreeView/BundleDocumentNode.cs | 14 ++++++ .../TreeView/DocumentTreeViewConstants.cs | 3 ++ dnSpy/dnSpy/Documents/Tabs/NodeDecompiler.cs | 26 +++++++++++ .../TreeView/BundleDocumentNodeImpl.cs | 44 +++++++++++++++++++ .../TreeView/DefaultDsDocumentNodeProvider.cs | 5 +++ .../Documents/TreeView/DocumentTreeView.cs | 33 ++++++++++++++ 9 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/BundleDocumentNode.cs create mode 100644 dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs 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/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs index deec0721ca..dd4ec1dce6 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs @@ -39,6 +39,8 @@ 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 string Filename { @@ -322,7 +324,7 @@ public class DsBundleDocument : DsDocument, IDsPEDocument, IDisposable { /// /// The bundle represented by this document. /// - public SingleFileBundle Bundle { get; } + public override SingleFileBundle? SingleFileBundle { get; } ModuleCreationOptions opts; @@ -334,16 +336,17 @@ public class DsBundleDocument : DsDocument, IDsPEDocument, IDisposable { public DsBundleDocument(IPEImage peImage, SingleFileBundle bundle, ModuleCreationOptions options) { PEImage = peImage; Filename = peImage.Filename ?? string.Empty; - Bundle = bundle; + SingleFileBundle = bundle; opts = options; } /// protected override TList CreateChildren() { var list = new TList(); - foreach (var entry in Bundle.Entries) { + foreach (var entry in SingleFileBundle!.Entries) { if (entry.Type == BundleFileType.Assembly) { - list.Add(DsDotNetDocument.CreateAssembly(DsDocumentInfo.CreateInMemory(() => (entry.Data, true), null), ModuleDefMD.Load(entry.Data, opts), true)); + list.Add(DsDotNetDocument.CreateAssembly(DsDocumentInfo.CreateInMemory(() => (entry.Data, true), null), + ModuleDefMD.Load(entry.Data, opts), true)); } } diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/IDsDocument.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/IDsDocument.cs index 7165fbcaec..d7405f1254 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/IDsDocument.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/IDsDocument.cs @@ -55,6 +55,11 @@ 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/sets the filename /// 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/DocumentTreeViewConstants.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/DocumentTreeViewConstants.cs index 1ff8c02f58..ef98dd7ad8 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/DocumentTreeViewConstants.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/DocumentTreeViewConstants.cs @@ -45,6 +45,9 @@ 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 RESOURCES_FOLDER_NODE_GUID = "1DD75445-9DED-482F-B6EB-4FD13E4A2197"; diff --git a/dnSpy/dnSpy/Documents/Tabs/NodeDecompiler.cs b/dnSpy/dnSpy/Documents/Tabs/NodeDecompiler.cs index b2599e8e04..b94faa1c80 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,25 @@ 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); + } + + //TODO: Write information about the bundle. + } + static NodeType GetNodeType(DocumentTreeNodeData node) { NodeType nodeType; var type = node.GetType(); @@ -334,6 +358,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..738cdb92c6 --- /dev/null +++ b/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +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 IEnumerable CreateChildren() { + Debug2.Assert(Document.SingleFileBundle is not null); + foreach (var document in Document.Children) + yield return Context.DocumentTreeView.CreateNode(this, document); + + // 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/DefaultDsDocumentNodeProvider.cs b/dnSpy/dnSpy/Documents/TreeView/DefaultDsDocumentNodeProvider.cs index 8ccaf8094d..05682663e4 100644 --- a/dnSpy/dnSpy/Documents/TreeView/DefaultDsDocumentNodeProvider.cs +++ b/dnSpy/dnSpy/Documents/TreeView/DefaultDsDocumentNodeProvider.cs @@ -25,6 +25,11 @@ 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 && owner.Document is not DsBundleDocument) diff --git a/dnSpy/dnSpy/Documents/TreeView/DocumentTreeView.cs b/dnSpy/dnSpy/Documents/TreeView/DocumentTreeView.cs index 52eaa57786..709dde8297 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 n.TreeNode.DataChildren.OfType()) { + if (a.Document.AssemblyDef == asm) + return a; + } + } + return null; } @@ -495,6 +504,18 @@ public FieldNode Create(FieldDef field) => return n; } + // Check for bundles + foreach (var n in TopNodes.OfType()) { + n.TreeNode.EnsureChildrenLoaded(); + foreach (var a in n.TreeNode.DataChildren.OfType()) { + a.TreeNode.EnsureChildrenLoaded(); + foreach (var m in a.TreeNode.DataChildren.OfType()) { + if (m.Document.ModuleDef == mod) + return m; + } + } + } + return null; } @@ -660,6 +681,18 @@ public IEnumerable GetAllModuleNodes() { } continue; } + + if (node is BundleDocumentNode bundleNode) { + bundleNode.TreeNode.EnsureChildrenLoaded(); + foreach (var a in bundleNode.TreeNode.DataChildren.OfType()) { + a.TreeNode.EnsureChildrenLoaded(); + foreach (var m in a.TreeNode.DataChildren.OfType()) { + yield return m; + } + } + + continue; + } } } From 50915156105de421ec4a64efddf24c81a36758a6 Mon Sep 17 00:00:00 2001 From: ElektroKill Date: Sat, 18 Dec 2021 13:49:17 +0100 Subject: [PATCH 03/11] Show folder structure in the treeview. --- .../Documents/{ => Bundles}/BundleEntry.cs | 12 ++++- .../Documents/{ => Bundles}/BundleFileType.cs | 0 .../Documents/Bundles/BundleFolder.cs | 25 +++++++++ .../{ => Bundles}/SingleFileBundle.cs | 37 ++++++++++++- .../Documents/DsDocument.cs | 18 +++++-- .../Documents/TreeView/BundleFolderNode.cs | 13 +++++ .../TreeView/DocumentTreeViewConstants.cs | 3 ++ dnSpy/dnSpy/Documents/Tabs/NodeDecompiler.cs | 11 +++- .../TreeView/BundleDocumentNodeImpl.cs | 15 +++++- .../TreeView/BundleFolderNodeImpl.cs | 53 +++++++++++++++++++ 10 files changed, 177 insertions(+), 10 deletions(-) rename dnSpy/dnSpy.Contracts.DnSpy/Documents/{ => Bundles}/BundleEntry.cs (86%) rename dnSpy/dnSpy.Contracts.DnSpy/Documents/{ => Bundles}/BundleFileType.cs (100%) create mode 100644 dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleFolder.cs rename dnSpy/dnSpy.Contracts.DnSpy/Documents/{ => Bundles}/SingleFileBundle.cs (74%) create mode 100644 dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/BundleFolderNode.cs create mode 100644 dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/BundleEntry.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleEntry.cs similarity index 86% rename from dnSpy/dnSpy.Contracts.DnSpy/Documents/BundleEntry.cs rename to dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleEntry.cs index 19ca6d2dcc..f000b1c363 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/BundleEntry.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleEntry.cs @@ -18,6 +18,16 @@ public sealed class BundleEntry { /// public string RelativePath { get; } + /// + /// The file name of the entry. + /// + public string? FileName { get; internal set; } + + /// + /// Docuemnt assosciated with this entry. + /// + public IDsDocument? Document { get; internal set; } + /// /// The raw data of the entry. /// @@ -25,7 +35,7 @@ public sealed class BundleEntry { BundleEntry(BundleFileType type, string relativePath, byte[] data) { Type = type; - RelativePath = relativePath; + RelativePath = relativePath.Replace('/', '\\'); Data = data; } diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/BundleFileType.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleFileType.cs similarity index 100% rename from dnSpy/dnSpy.Contracts.DnSpy/Documents/BundleFileType.cs rename to dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleFileType.cs diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleFolder.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleFolder.cs new file mode 100644 index 0000000000..40ebfd7779 --- /dev/null +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleFolder.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace dnSpy.Contracts.Documents { + /// + /// Represents one folder in a + /// + public sealed class BundleFolder { + /// + /// Gets the short name of the folder. + /// + public string Name { get; } + + internal BundleFolder(string name) => Name = name; + + /// + /// The folders nested within this folder. + /// + public List Folders { get; } = new List(); + + /// + /// The entires in this folder. + /// + public List Entries { get; } = new List(); + } +} diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/SingleFileBundle.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/SingleFileBundle.cs similarity index 74% rename from dnSpy/dnSpy.Contracts.DnSpy/Documents/SingleFileBundle.cs rename to dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/SingleFileBundle.cs index 1c04230d3c..72cbbc5e91 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/SingleFileBundle.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/SingleFileBundle.cs @@ -69,10 +69,20 @@ public sealed class SingleFileBundle { public ulong Flags { get; } /// - /// The entries present in the bundle + /// All of the entries present in the bundle /// public IReadOnlyList Entries { get; } + /// + /// The top level entries present in the bundle + /// + public IReadOnlyList TopLevelEntries { get; } + + /// + /// The top level folders present in the bundle. + /// + public IReadOnlyList TopLevelFolders { get; } + SingleFileBundle(DataReader reader, uint major, uint minor) { MajorVersion = major; MinorVersion = minor; @@ -87,6 +97,31 @@ public sealed class SingleFileBundle { } Entries = BundleEntry.ReadEntries(reader, EntryCount, MajorVersion >= 6); + + var rootFolder = new BundleFolder(""); + var folders = new Dictionary { { "", 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; + } } /// diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs index dd4ec1dce6..d3aa744047 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs @@ -20,6 +20,7 @@ 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.Utilities; @@ -326,16 +327,19 @@ public class DsBundleDocument : DsDocument, IDsPEDocument, IDisposable { /// public override SingleFileBundle? SingleFileBundle { get; } - ModuleCreationOptions opts; + readonly ModuleCreationOptions opts; + readonly string directoryOfBundle; /// /// Constructor /// /// PE image /// Parsed bundle + /// Module creation options public DsBundleDocument(IPEImage peImage, SingleFileBundle bundle, ModuleCreationOptions options) { PEImage = peImage; Filename = peImage.Filename ?? string.Empty; + directoryOfBundle = Path.GetDirectoryName(Filename) ?? string.Empty; SingleFileBundle = bundle; opts = options; } @@ -345,8 +349,14 @@ protected override TList CreateChildren() { var list = new TList(); foreach (var entry in SingleFileBundle!.Entries) { if (entry.Type == BundleFileType.Assembly) { - list.Add(DsDotNetDocument.CreateAssembly(DsDocumentInfo.CreateInMemory(() => (entry.Data, true), null), - ModuleDefMD.Load(entry.Data, opts), true)); + var mod = ModuleDefMD.Load(entry.Data, opts); + mod.Location = Path.Combine(directoryOfBundle, entry.RelativePath); + + var document = entry.Document = DsDotNetDocument.CreateAssembly( + DsDocumentInfo.CreateInMemory(() => (entry.Data, true), mod.Location), + mod, true); + + list.Add(document); } } @@ -365,7 +375,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/TreeView/BundleFolderNode.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/BundleFolderNode.cs new file mode 100644 index 0000000000..6359d6f9ae --- /dev/null +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/BundleFolderNode.cs @@ -0,0 +1,13 @@ +using System.Diagnostics; + +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 ef98dd7ad8..8c34e08cf4 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/DocumentTreeViewConstants.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/DocumentTreeViewConstants.cs @@ -48,6 +48,9 @@ public static class DocumentTreeViewConstants { /// 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 RESOURCES_FOLDER_NODE_GUID = "1DD75445-9DED-482F-B6EB-4FD13E4A2197"; diff --git a/dnSpy/dnSpy/Documents/Tabs/NodeDecompiler.cs b/dnSpy/dnSpy/Documents/Tabs/NodeDecompiler.cs index b94faa1c80..2eb7d3bd80 100644 --- a/dnSpy/dnSpy/Documents/Tabs/NodeDecompiler.cs +++ b/dnSpy/dnSpy/Documents/Tabs/NodeDecompiler.cs @@ -296,8 +296,15 @@ void Decompile(BundleDocumentNode node) { timestampLine += $"{dnSpy_Resources.UnknownValue} ({ts:X8})"; decompiler.WriteCommentLine(output, timestampLine); } - - //TODO: Write information about the bundle. + 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.Entries.Count}"); + } } static NodeType GetNodeType(DocumentTreeNodeData node) { diff --git a/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs b/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs index 738cdb92c6..15025f61dd 100644 --- a/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs +++ b/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs @@ -19,8 +19,19 @@ public BundleDocumentNodeImpl(IDsDocument document) : base(document) { } public override IEnumerable CreateChildren() { Debug2.Assert(Document.SingleFileBundle is not null); - foreach (var document in Document.Children) - yield return Context.DocumentTreeView.CreateNode(this, document); + + // 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; + + foreach (var bundleFolder in Document.SingleFileBundle.TopLevelFolders) { + yield return new BundleFolderNodeImpl(this, bundleFolder); + } + + foreach (var entry in Document.SingleFileBundle.TopLevelEntries) { + if (entry.Document is not null) + yield return Context.DocumentTreeView.CreateNode(this, entry.Document); + } // TODO: return all bundle entries } diff --git a/dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs b/dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs new file mode 100644 index 0000000000..122b608624 --- /dev/null +++ b/dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +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); + + 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.Folders) { + yield return new BundleFolderNodeImpl(owner, folder); + } + + foreach (var entry in bundleFolder.Entries) { + if (entry.Document is not null) + yield return Context.DocumentTreeView.CreateNode(owner, entry.Document); + } + } + + 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.Folders.Count != 0) { + // TODO: localize string + output.Write(BoxedTextColor.Text, $"Subfolders: {bundleFolder.Folders.Count}"); + } + } + } + } +} From 72c26e0f27d5b7eddfe4fddee746f4c6eac08478 Mon Sep 17 00:00:00 2001 From: ElektroKill Date: Sat, 18 Dec 2021 13:49:42 +0100 Subject: [PATCH 04/11] Load native binaries as child documents --- dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs index d3aa744047..0bf86e7fe5 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs @@ -358,6 +358,8 @@ protected override TList CreateChildren() { list.Add(document); } + else if (entry.Type == BundleFileType.NativeBinary) + list.Add(new DsPEDocument(new PEImage(entry.Data, Path.Combine(directoryOfBundle, entry.RelativePath)))); } return list; From 87f678dcc4d91acbf9108aff14e67776df48a94e Mon Sep 17 00:00:00 2001 From: ElektroKill Date: Sat, 18 Dec 2021 14:01:08 +0100 Subject: [PATCH 05/11] Fix navigation for assemblies inside bundle folders --- .../Documents/TreeView/DocumentTreeView.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/dnSpy/dnSpy/Documents/TreeView/DocumentTreeView.cs b/dnSpy/dnSpy/Documents/TreeView/DocumentTreeView.cs index 709dde8297..b9622551df 100644 --- a/dnSpy/dnSpy/Documents/TreeView/DocumentTreeView.cs +++ b/dnSpy/dnSpy/Documents/TreeView/DocumentTreeView.cs @@ -477,7 +477,7 @@ public FieldNode Create(FieldDef field) => // Check for bundles foreach (var n in TopNodes.OfType()) { n.TreeNode.EnsureChildrenLoaded(); - foreach (var a in n.TreeNode.DataChildren.OfType()) { + foreach (var a in GetAllBundleAssemblies(n)) { if (a.Document.AssemblyDef == asm) return a; } @@ -506,8 +506,7 @@ public FieldNode Create(FieldDef field) => // Check for bundles foreach (var n in TopNodes.OfType()) { - n.TreeNode.EnsureChildrenLoaded(); - foreach (var a in n.TreeNode.DataChildren.OfType()) { + foreach (var a in GetAllBundleAssemblies(n)) { a.TreeNode.EnsureChildrenLoaded(); foreach (var m in a.TreeNode.DataChildren.OfType()) { if (m.Document.ModuleDef == mod) @@ -519,6 +518,18 @@ public FieldNode Create(FieldDef field) => 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; @@ -683,8 +694,7 @@ public IEnumerable GetAllModuleNodes() { } if (node is BundleDocumentNode bundleNode) { - bundleNode.TreeNode.EnsureChildrenLoaded(); - foreach (var a in bundleNode.TreeNode.DataChildren.OfType()) { + foreach (var a in GetAllBundleAssemblies(bundleNode)) { a.TreeNode.EnsureChildrenLoaded(); foreach (var m in a.TreeNode.DataChildren.OfType()) { yield return m; From 02d7a638a6e5bf5a747dcef229ac313f6a5e9f58 Mon Sep 17 00:00:00 2001 From: ElektroKill Date: Sat, 18 Dec 2021 14:01:34 +0100 Subject: [PATCH 06/11] Enable lazy loading of bundle treeview nodes --- dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs | 2 +- dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs b/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs index 15025f61dd..b1bea49a07 100644 --- a/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs +++ b/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs @@ -14,8 +14,8 @@ 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); diff --git a/dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs b/dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs index 122b608624..d56accc11a 100644 --- a/dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs +++ b/dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs @@ -13,6 +13,7 @@ sealed class BundleFolderNodeImpl : BundleFolderNode { 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; From 37fb18bf121b5849d42fa55677b48bb45997e8b8 Mon Sep 17 00:00:00 2001 From: ElektroKill Date: Sat, 18 Dec 2021 14:27:54 +0100 Subject: [PATCH 07/11] Fix remaining navigation issues --- dnSpy/dnSpy/Documents/DsDocumentService.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dnSpy/dnSpy/Documents/DsDocumentService.cs b/dnSpy/dnSpy/Documents/DsDocumentService.cs index ace44b01f1..ed856aaa2c 100644 --- a/dnSpy/dnSpy/Documents/DsDocumentService.cs +++ b/dnSpy/dnSpy/Documents/DsDocumentService.cs @@ -163,6 +163,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 +231,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; } From d3d72238f3a2406eddaf3cdca003cfb095650e91 Mon Sep 17 00:00:00 2001 From: ElektroKill Date: Tue, 21 Dec 2021 21:52:13 +0100 Subject: [PATCH 08/11] Allow lazy loading of bunde entry data --- .../Documents/Bundles/BundleEntry.cs | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleEntry.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleEntry.cs index f000b1c363..4f95444363 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleEntry.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleEntry.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; +using System.Threading; using dnlib.IO; namespace dnSpy.Contracts.Documents { @@ -8,6 +9,9 @@ namespace dnSpy.Contracts.Documents { /// Represents one entry in a /// public sealed class BundleEntry { + byte[]? data; + DataReader reader; + /// /// Type of the entry /// @@ -31,12 +35,25 @@ public sealed class BundleEntry { /// /// The raw data of the entry. /// - public byte[] Data { get; } + 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, byte[] data) { Type = type; RelativePath = relativePath.Replace('/', '\\'); - Data = data; + this.data = data; + } + + BundleEntry(BundleFileType type, string relativePath, DataReader reader) { + Type = type; + RelativePath = relativePath.Replace('/', '\\'); + this.reader = reader; } internal static IReadOnlyList ReadEntries(DataReader reader, int count, bool allowCompression) { @@ -49,18 +66,16 @@ internal static IReadOnlyList ReadEntries(DataReader reader, int co var type = (BundleFileType)reader.ReadByte(); string path = reader.ReadSerializedString(); - res[i] = new BundleEntry(type, path, ReadEntryData(reader, offset, size, compSize)); + if (compSize == 0) + res[i] = new BundleEntry(type, path, reader.Slice((uint)offset, (uint)size)); + else + res[i] = new BundleEntry(type, path, ReadCompressedEntryData(reader, offset, size, compSize)); } return res; } - static byte[] ReadEntryData(DataReader reader, long offset, long size, long compSize) { - if (compSize == 0) { - reader.Position = (uint)offset; - return reader.ReadBytes((int)size); - } - + 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)) { From 073864d76c1f212c0f19c5517e33426001fa04f7 Mon Sep 17 00:00:00 2001 From: ElektroKill Date: Fri, 24 Dec 2021 10:50:06 +0100 Subject: [PATCH 09/11] Show all bundle entries in the tree view --- .../SaveModule/SaveModuleCommand.cs | 16 ++++++--- .../Documents/Bundles/BundleEntry.cs | 22 ++++++++++--- .../Documents/DsDocument.cs | 2 +- .../TreeView/DocumentTreeViewConstants.cs | 6 ++++ .../Documents/TreeView/JsonBundleEntryNode.cs | 13 ++++++++ .../TreeView/UnknownBundleEntryNode.cs | 13 ++++++++ .../TreeView/BundleDocumentNodeImpl.cs | 14 ++++++++ .../TreeView/BundleFolderNodeImpl.cs | 14 ++++++++ .../TreeView/JsonBundleEntryNodeImpl.cs | 33 +++++++++++++++++++ .../TreeView/UnknownBundleEntryNodeImpl.cs | 25 ++++++++++++++ 10 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/JsonBundleEntryNode.cs create mode 100644 dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/UnknownBundleEntryNode.cs create mode 100644 dnSpy/dnSpy/Documents/TreeView/JsonBundleEntryNodeImpl.cs create mode 100644 dnSpy/dnSpy/Documents/TreeView/UnknownBundleEntryNodeImpl.cs 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/Bundles/BundleEntry.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleEntry.cs index 4f95444363..8c02d9ceab 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleEntry.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleEntry.cs @@ -22,6 +22,16 @@ public sealed class BundleEntry { /// public string RelativePath { get; } + /// + /// The offset of the entry's data. + /// + public long Offset { get; } + + /// + /// The size of the entry's data. + /// + public long Size { get; } + /// /// The file name of the entry. /// @@ -44,15 +54,19 @@ public byte[] Data { } } - BundleEntry(BundleFileType type, string relativePath, byte[] 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, DataReader reader) { + BundleEntry(BundleFileType type, string relativePath, long offset, long size, DataReader reader) { Type = type; RelativePath = relativePath.Replace('/', '\\'); + Offset = offset; + Size = size; this.reader = reader; } @@ -67,9 +81,9 @@ internal static IReadOnlyList ReadEntries(DataReader reader, int co string path = reader.ReadSerializedString(); if (compSize == 0) - res[i] = new BundleEntry(type, path, reader.Slice((uint)offset, (uint)size)); + res[i] = new BundleEntry(type, path, offset, size, reader.Slice((uint)offset, (uint)size)); else - res[i] = new BundleEntry(type, path, ReadCompressedEntryData(reader, offset, size, compSize)); + res[i] = new BundleEntry(type, path, offset, size, ReadCompressedEntryData(reader, offset, size, compSize)); } return res; diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs index 0bf86e7fe5..2de657b04e 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs @@ -359,7 +359,7 @@ protected override TList CreateChildren() { list.Add(document); } else if (entry.Type == BundleFileType.NativeBinary) - list.Add(new DsPEDocument(new PEImage(entry.Data, Path.Combine(directoryOfBundle, entry.RelativePath)))); + list.Add(entry.Document = new DsPEDocument(new PEImage(entry.Data, Path.Combine(directoryOfBundle, entry.RelativePath)))); } return list; diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/DocumentTreeViewConstants.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/DocumentTreeViewConstants.cs index 8c34e08cf4..715ec0ad3d 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/DocumentTreeViewConstants.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/DocumentTreeViewConstants.cs @@ -51,6 +51,12 @@ public static class DocumentTreeViewConstants { /// 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..bdef121820 --- /dev/null +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/JsonBundleEntryNode.cs @@ -0,0 +1,13 @@ +using System.Diagnostics; + +namespace dnSpy.Contracts.Documents.TreeView { + /// + /// JSON bundle entry node + /// + public abstract class JsonBundleEntryNode : DocumentTreeNodeData { + /// + /// Constructor + /// + protected JsonBundleEntryNode(BundleEntry bundleEntry) => Debug2.Assert(bundleEntry is not null); + } +} 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..b1338f7cbb --- /dev/null +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/UnknownBundleEntryNode.cs @@ -0,0 +1,13 @@ +using System.Diagnostics; + +namespace dnSpy.Contracts.Documents.TreeView { + /// + /// Unknown bundle entry node + /// + public abstract class UnknownBundleEntryNode : DocumentTreeNodeData { + /// + /// Constructor + /// + protected UnknownBundleEntryNode(BundleEntry bundleEntry) => Debug2.Assert(bundleEntry is not null); + } +} diff --git a/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs b/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs index b1bea49a07..b60e0af9b1 100644 --- a/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs +++ b/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs @@ -31,6 +31,20 @@ public override IEnumerable CreateChildren() { foreach (var entry in Document.SingleFileBundle.TopLevelEntries) { if (entry.Document is not null) yield return Context.DocumentTreeView.CreateNode(this, entry.Document); + else { + switch (entry.Type) { + case BundleFileType.Unknown: + case BundleFileType.Symbols: + yield return new UnknownBundleEntryNodeImpl(entry); + break; + case BundleFileType.DepsJson: + case BundleFileType.RuntimeConfigJson: + yield return new JsonBundleEntryNodeImpl(entry); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } } // TODO: return all bundle entries diff --git a/dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs b/dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs index d56accc11a..24bdaae2a7 100644 --- a/dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs +++ b/dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs @@ -31,6 +31,20 @@ public override IEnumerable CreateChildren() { foreach (var entry in bundleFolder.Entries) { if (entry.Document is not null) yield return Context.DocumentTreeView.CreateNode(owner, entry.Document); + else { + switch (entry.Type) { + case BundleFileType.Unknown: + case BundleFileType.Symbols: + yield return new UnknownBundleEntryNodeImpl(entry); + break; + case BundleFileType.DepsJson: + case BundleFileType.RuntimeConfigJson: + yield return new JsonBundleEntryNodeImpl(entry); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } } } diff --git a/dnSpy/dnSpy/Documents/TreeView/JsonBundleEntryNodeImpl.cs b/dnSpy/dnSpy/Documents/TreeView/JsonBundleEntryNodeImpl.cs new file mode 100644 index 0000000000..2eebd3d486 --- /dev/null +++ b/dnSpy/dnSpy/Documents/TreeView/JsonBundleEntryNodeImpl.cs @@ -0,0 +1,33 @@ +using System; +using System.Text; +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(Encoding.UTF8.GetString(bundleEntry.Data), 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..8b30d575e1 --- /dev/null +++ b/dnSpy/dnSpy/Documents/TreeView/UnknownBundleEntryNodeImpl.cs @@ -0,0 +1,25 @@ +using System; +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); + } + } +} From e5dc3b4f64deddbf1356b897c599fd3fa3f67524 Mon Sep 17 00:00:00 2001 From: ElektroKill Date: Sun, 5 Nov 2023 13:05:33 +0100 Subject: [PATCH 10/11] Rewrote bundle object model and reading code --- .../Documents/Bundles/BundleEntry.cs | 103 ------- .../Documents/Bundles/BundleFolder.cs | 25 -- .../Documents/Bundles/SingleFileBundle.cs | 168 ----------- .../Documents/DsDocument.cs | 44 ++- .../Documents/IDsDocument.cs | 6 + .../Documents/TreeView/BundleFolderNode.cs | 1 + .../Documents/TreeView/JsonBundleEntryNode.cs | 1 + .../TreeView/UnknownBundleEntryNode.cs | 1 + .../Bundles/BundleEntry.cs | 134 +++++++++ .../Bundles/BundleEntryMD.cs | 166 +++++++++++ .../Bundles/BundleEntryMDUtils.cs | 22 ++ .../Bundles/BundleEntryType.cs} | 6 +- .../Bundles/BundleEntryUser.cs | 3 + .../Bundles/BundleExtensions.cs | 30 ++ .../Bundles/BundleFolder.cs | 85 ++++++ .../Bundles/SingleFileBundle.cs | 278 ++++++++++++++++++ dnSpy/dnSpy/Documents/DsDocumentService.cs | 7 +- dnSpy/dnSpy/Documents/Tabs/NodeDecompiler.cs | 2 +- .../TreeView/BundleDocumentNodeImpl.cs | 24 +- .../TreeView/BundleFolderNodeImpl.cs | 28 +- .../TreeView/JsonBundleEntryNodeImpl.cs | 3 +- .../TreeView/UnknownBundleEntryNodeImpl.cs | 1 + 22 files changed, 803 insertions(+), 335 deletions(-) delete mode 100644 dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleEntry.cs delete mode 100644 dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleFolder.cs delete mode 100644 dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/SingleFileBundle.cs create mode 100644 dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntry.cs create mode 100644 dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryMD.cs create mode 100644 dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryMDUtils.cs rename dnSpy/{dnSpy.Contracts.DnSpy/Documents/Bundles/BundleFileType.cs => dnSpy.Contracts.Logic/Bundles/BundleEntryType.cs} (80%) create mode 100644 dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryUser.cs create mode 100644 dnSpy/dnSpy.Contracts.Logic/Bundles/BundleExtensions.cs create mode 100644 dnSpy/dnSpy.Contracts.Logic/Bundles/BundleFolder.cs create mode 100644 dnSpy/dnSpy.Contracts.Logic/Bundles/SingleFileBundle.cs diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleEntry.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleEntry.cs deleted file mode 100644 index 8c02d9ceab..0000000000 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleEntry.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Threading; -using dnlib.IO; - -namespace dnSpy.Contracts.Documents { - /// - /// Represents one entry in a - /// - public sealed class BundleEntry { - byte[]? data; - DataReader reader; - - /// - /// Type of the entry - /// - public BundleFileType Type { get; } - - /// - /// Path of an embedded file, relative to the Bundle source-directory. - /// - public string RelativePath { get; } - - /// - /// The offset of the entry's data. - /// - public long Offset { get; } - - /// - /// The size of the entry's data. - /// - public long Size { get; } - - /// - /// The file name of the entry. - /// - public string? FileName { get; internal set; } - - /// - /// Docuemnt assosciated with this entry. - /// - public IDsDocument? Document { get; internal set; } - - /// - /// The raw data of the entry. - /// - 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 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(); - } - } - } - } - } -} diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleFolder.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleFolder.cs deleted file mode 100644 index 40ebfd7779..0000000000 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleFolder.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; - -namespace dnSpy.Contracts.Documents { - /// - /// Represents one folder in a - /// - public sealed class BundleFolder { - /// - /// Gets the short name of the folder. - /// - public string Name { get; } - - internal BundleFolder(string name) => Name = name; - - /// - /// The folders nested within this folder. - /// - public List Folders { get; } = new List(); - - /// - /// The entires in this folder. - /// - public List Entries { get; } = new List(); - } -} diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/SingleFileBundle.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/SingleFileBundle.cs deleted file mode 100644 index 72cbbc5e91..0000000000 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/SingleFileBundle.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using dnlib.IO; -using dnlib.PE; - -namespace dnSpy.Contracts.Documents { - /// - /// Class for dealing with .NET 5 single-file bundles. - /// - /// Based on code from Microsoft.NET.HostModel. - /// - 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 - }; - - /// - /// 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; } - - /// - /// Offset of the embedded *.deps.json file. - /// Only present in version 2.0 and above. - /// - public long DepsJsonOffset { get; } - - /// - /// Size of the embedded *.deps.json file. - /// Only present in version 2.0 and above. - /// - public long DepsJsonSize { get; } - - /// - /// Offset of the embedded *.runtimeconfig.json file. - /// Only present in version 2.0 and above. - /// - public long RuntimeConfigJsonOffset { get; } - - /// - /// Size of the embedded *.runtimeconfig.json file. - /// Only present in version 2.0 and above. - /// - public long RuntimeConfigJsonSize { get; } - - /// - /// Bundle flags - /// Only present in version 2.0 and above. - /// - public ulong Flags { get; } - - /// - /// All of the entries present in the bundle - /// - public IReadOnlyList Entries { get; } - - /// - /// The top level entries present in the bundle - /// - public IReadOnlyList TopLevelEntries { get; } - - /// - /// The top level folders present in the bundle. - /// - public IReadOnlyList 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 { { "", 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; - } - } - - /// - /// Parses a bundle from the provided - /// - /// The - /// The or null if its not a bundle. - 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; - } - } -} diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs index 2de657b04e..a1716ba804 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/DsDocument.cs @@ -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; /// public virtual SingleFileBundle? SingleFileBundle => null; + /// + public virtual BundleEntry? BundleEntry => null; /// public string Filename { @@ -138,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 @@ -148,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(); } @@ -229,6 +237,10 @@ public class DsDotNetDocument : DsDotNetDocumentBase, IDisposable { public override DsDocumentInfo? SerializedDocument => documentInfo; DsDocumentInfo documentInfo; + /// + public override BundleEntry? BundleEntry => bundleEntry; + BundleEntry? bundleEntry; + /// /// Constructor /// @@ -292,6 +304,8 @@ protected override TList CreateChildren() { return list; } + internal void SetBundleEntry(BundleEntry bundleEntry) => this.bundleEntry = bundleEntry; + /// public void Dispose() => ModuleDef!.Dispose(); } @@ -327,7 +341,6 @@ public class DsBundleDocument : DsDocument, IDsPEDocument, IDisposable { /// public override SingleFileBundle? SingleFileBundle { get; } - readonly ModuleCreationOptions opts; readonly string directoryOfBundle; /// @@ -335,31 +348,38 @@ public class DsBundleDocument : DsDocument, IDsPEDocument, IDisposable { /// /// PE image /// Parsed bundle - /// Module creation options - 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; } /// protected override TList CreateChildren() { var list = new TList(); 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; diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/IDsDocument.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/IDsDocument.cs index d7405f1254..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 { /// @@ -60,6 +61,11 @@ public interface IDsDocument : IAnnotations { /// 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/BundleFolderNode.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/BundleFolderNode.cs index 6359d6f9ae..1e8d9225fd 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/BundleFolderNode.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/BundleFolderNode.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using dnSpy.Contracts.Bundles; namespace dnSpy.Contracts.Documents.TreeView { /// diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/JsonBundleEntryNode.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/JsonBundleEntryNode.cs index bdef121820..fe8c70f844 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/JsonBundleEntryNode.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/JsonBundleEntryNode.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using dnSpy.Contracts.Bundles; namespace dnSpy.Contracts.Documents.TreeView { /// diff --git a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/UnknownBundleEntryNode.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/UnknownBundleEntryNode.cs index b1338f7cbb..c22f0de133 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/UnknownBundleEntryNode.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/UnknownBundleEntryNode.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using dnSpy.Contracts.Bundles; namespace dnSpy.Contracts.Documents.TreeView { /// 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.DnSpy/Documents/Bundles/BundleFileType.cs b/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryType.cs similarity index 80% rename from dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleFileType.cs rename to dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryType.cs index 4f49c23da8..d4f7624744 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/Bundles/BundleFileType.cs +++ b/dnSpy/dnSpy.Contracts.Logic/Bundles/BundleEntryType.cs @@ -1,11 +1,11 @@ -namespace dnSpy.Contracts.Documents { +namespace dnSpy.Contracts.Bundles { /// - /// 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. /// - public enum BundleFileType : byte { + public enum BundleEntryType : byte { /// /// Type not determined. /// 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 ed856aaa2c..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; @@ -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); diff --git a/dnSpy/dnSpy/Documents/Tabs/NodeDecompiler.cs b/dnSpy/dnSpy/Documents/Tabs/NodeDecompiler.cs index 2eb7d3bd80..cb5c5bc54d 100644 --- a/dnSpy/dnSpy/Documents/Tabs/NodeDecompiler.cs +++ b/dnSpy/dnSpy/Documents/Tabs/NodeDecompiler.cs @@ -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}"); } } diff --git a/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs b/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs index b60e0af9b1..0f2ad31040 100644 --- a/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs +++ b/dnSpy/dnSpy/Documents/TreeView/BundleDocumentNodeImpl.cs @@ -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 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(); + 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: diff --git a/dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs b/dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs index 24bdaae2a7..1c772ad27e 100644 --- a/dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs +++ b/dnSpy/dnSpy/Documents/TreeView/BundleFolderNodeImpl.cs @@ -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 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(); + 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}"); } } } diff --git a/dnSpy/dnSpy/Documents/TreeView/JsonBundleEntryNodeImpl.cs b/dnSpy/dnSpy/Documents/TreeView/JsonBundleEntryNodeImpl.cs index 2eebd3d486..ce6506983b 100644 --- a/dnSpy/dnSpy/Documents/TreeView/JsonBundleEntryNodeImpl.cs +++ b/dnSpy/dnSpy/Documents/TreeView/JsonBundleEntryNodeImpl.cs @@ -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; } } diff --git a/dnSpy/dnSpy/Documents/TreeView/UnknownBundleEntryNodeImpl.cs b/dnSpy/dnSpy/Documents/TreeView/UnknownBundleEntryNodeImpl.cs index 8b30d575e1..87c16ea353 100644 --- a/dnSpy/dnSpy/Documents/TreeView/UnknownBundleEntryNodeImpl.cs +++ b/dnSpy/dnSpy/Documents/TreeView/UnknownBundleEntryNodeImpl.cs @@ -1,4 +1,5 @@ using System; +using dnSpy.Contracts.Bundles; using dnSpy.Contracts.Decompiler; using dnSpy.Contracts.Documents; using dnSpy.Contracts.Documents.TreeView; From 6d0f6095163c2b15506523ae804902b438757a86 Mon Sep 17 00:00:00 2001 From: ElektroKill Date: Sat, 30 Dec 2023 20:16:10 +0100 Subject: [PATCH 11/11] Add `IBundleEntryNode` interface --- .../TreeView/AssemblyDocumentNode.cs | 4 ++- .../Documents/TreeView/JsonBundleEntryNode.cs | 11 +++++-- .../Documents/TreeView/ModuleDocumentNode.cs | 4 ++- .../Documents/TreeView/PEDocumentNode.cs | 6 +++- .../TreeView/UnknownBundleEntryNode.cs | 11 +++++-- .../TreeView/IBundleEntryNode.cs | 32 +++++++++++++++++++ 6 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 dnSpy/dnSpy.Contracts.DnSpy/TreeView/IBundleEntryNode.cs 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/JsonBundleEntryNode.cs b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/JsonBundleEntryNode.cs index fe8c70f844..0d46eb5ba0 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/JsonBundleEntryNode.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/JsonBundleEntryNode.cs @@ -1,14 +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 { + public abstract class JsonBundleEntryNode : DocumentTreeNodeData, IBundleEntryNode { + /// + public BundleEntry BundleEntry { get; } + /// /// Constructor /// - protected JsonBundleEntryNode(BundleEntry bundleEntry) => Debug2.Assert(bundleEntry is not null); + 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 index c22f0de133..6b1af0a07b 100644 --- a/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/UnknownBundleEntryNode.cs +++ b/dnSpy/dnSpy.Contracts.DnSpy/Documents/TreeView/UnknownBundleEntryNode.cs @@ -1,14 +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 { + public abstract class UnknownBundleEntryNode : DocumentTreeNodeData, IBundleEntryNode { + /// + public BundleEntry BundleEntry { get; } + /// /// Constructor /// - protected UnknownBundleEntryNode(BundleEntry bundleEntry) => Debug2.Assert(bundleEntry is not null); + 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; } + } +}