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); + } + } +}