├── .gitignore ├── Casks └── malimite.rb ├── DecompilerBridge └── ghidra │ └── DumpClassData.java ├── LICENSE ├── README.md ├── lib └── ghidra.jar ├── media ├── gatekeeper_allow.png ├── malimite_dependencies.png ├── malimite_features_github.png ├── malimite_gatekeeper.png ├── malimite_ghidra_path.png ├── malimite_logo.png └── malimite_open.png ├── pom.xml └── src └── main ├── antlr4 ├── CPP14Lexer.g4 └── CPP14Parser.g4 ├── java └── com │ └── lauriewired │ └── malimite │ ├── Malimite.java │ ├── configuration │ ├── Config.java │ ├── LibraryDefinitions.java │ └── Project.java │ ├── database │ └── SQLiteDBHandler.java │ ├── decompile │ ├── DemangleSwift.java │ ├── DynamicDecompile.java │ ├── GhidraProject.java │ ├── SyntaxParser.java │ └── antlr │ │ ├── CPP14Lexer.interp │ │ ├── CPP14Lexer.java │ │ ├── CPP14Lexer.tokens │ │ ├── CPP14Parser.interp │ │ ├── CPP14Parser.java │ │ ├── CPP14Parser.tokens │ │ ├── CPP14ParserBase.java │ │ ├── CPP14ParserBaseListener.java │ │ ├── CPP14ParserBaseVisitor.java │ │ ├── CPP14ParserListener.java │ │ └── CPP14ParserVisitor.java │ ├── files │ ├── InfoPlist.java │ ├── Macho.java │ └── MobileProvision.java │ ├── security │ └── KeyEncryption.java │ ├── tools │ ├── AIBackend.java │ └── RuntimeMethodHandler.java │ ├── ui │ ├── AnalysisWindow.java │ ├── ApplicationMenu.java │ ├── CustomTokenMaker.java │ ├── EntrypointsDialog.java │ ├── KeyboardShortcuts.java │ ├── LibraryConfigDialog.java │ ├── PreferencesDialog.java │ ├── ReferenceHandler.java │ ├── ReferencesDialog.java │ ├── SafeMenuAction.java │ ├── SearchResultsDialog.java │ ├── SelectFile.java │ ├── SyntaxHighlighter.java │ └── WrapLayout.java │ └── utils │ ├── FileProcessing.java │ ├── GhidraSetup.java │ ├── NodeOperations.java │ ├── PlistUtils.java │ └── ResourceParser.java └── resources └── icons └── app-icon.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore build output directories 2 | target/ 3 | build/ 4 | out/ 5 | 6 | # Ignore VS Code settings 7 | .vscode/ 8 | *.code-workspace 9 | 10 | # Ignore macOS system files 11 | .DS_Store 12 | .AppleDouble 13 | .LSOverride 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # macOS resource forks 19 | .Spotlight-V100 20 | .Trashes 21 | .VolumeIcon.icns 22 | .Icon? 23 | 24 | # Java-specific files 25 | *.class 26 | *.jar 27 | !ghidra.jar 28 | *.war 29 | *.ear 30 | *.log 31 | 32 | # Maven/Gradle output 33 | .mvn/ 34 | .gradle/ 35 | build/ 36 | 37 | # Log files and system dumps 38 | *.log 39 | hs_err_pid*.log 40 | 41 | # Ignore IDE files 42 | .idea/ 43 | *.iml 44 | .classpath 45 | .project 46 | 47 | # VS Code workspace settings 48 | *.code-workspace 49 | .vscode/* 50 | 51 | # Ignore local configuration file 52 | malimite.properties 53 | malimite.projects 54 | -------------------------------------------------------------------------------- /Casks/malimite.rb: -------------------------------------------------------------------------------- 1 | cask "malimite" do 2 | version "1.1" 3 | sha256 "a74fd75844aedec13b523da6f8faaf9ec0c2a37027c4e372f74294ea07069528" 4 | 5 | url "https://github.com/LaurieWired/Malimite/releases/download/#{version}/Malimite-1-1.zip" 6 | name "Malimite" 7 | desc "Decompiler for Apple applications" 8 | homepage "https://github.com/LaurieWired/Malimite" 9 | 10 | depends_on formula: "java" 11 | 12 | postflight do 13 | libexec = "#{HOMEBREW_PREFIX}/libexec/malimite" 14 | bin = "#{HOMEBREW_PREFIX}/bin/malimite" 15 | 16 | FileUtils.mkdir_p libexec 17 | FileUtils.mv Dir["#{staged_path}/*"], libexec 18 | 19 | File.write(bin, <<~EOS) 20 | #!/bin/bash 21 | exec java -jar "#{libexec}/Malimite-1-1.jar" "$@" 22 | EOS 23 | FileUtils.chmod("+x", bin) 24 | end 25 | 26 | uninstall delete: [ 27 | "#{HOMEBREW_PREFIX}/bin/malimite", 28 | "#{HOMEBREW_PREFIX}/libexec/malimite", 29 | ] 30 | 31 | zap trash: [ 32 | "~/Library/Application Support/Malimite", 33 | "~/Library/Caches/Malimite", 34 | "~/Library/Logs/Malimite", 35 | "~/Library/Preferences/com.lauriewired.malimite.plist", 36 | "~/Library/Saved Application State/com.lauriewired.malimite.savedState", 37 | ] 38 | 39 | caveats <<~EOS 40 | Ghidra is a recommended dependency for Malimite. You can install it via: 41 | brew install --cask ghidra 42 | EOS 43 | end 44 | -------------------------------------------------------------------------------- /DecompilerBridge/ghidra/DumpClassData.java: -------------------------------------------------------------------------------- 1 | import ghidra.app.decompiler.DecompInterface; 2 | import ghidra.app.script.GhidraScript; 3 | import ghidra.program.model.symbol.Namespace; 4 | import ghidra.program.model.mem.Memory; 5 | import ghidra.program.model.mem.MemoryBlock; 6 | import ghidra.program.model.address.Address; 7 | import ghidra.util.task.ConsoleTaskMonitor; 8 | import ghidra.program.model.listing.*; 9 | import ghidra.program.model.data.DataType; 10 | import ghidra.program.model.data.StringDataType; 11 | 12 | import java.io.IOException; 13 | import java.io.PrintWriter; 14 | import java.net.Socket; 15 | import java.util.*; 16 | import org.json.JSONObject; 17 | import org.json.JSONArray; 18 | 19 | public class DumpClassData extends GhidraScript { 20 | 21 | private int port; 22 | private List libraryPrefixes; 23 | 24 | private void parseArgs() { 25 | String[] args = getScriptArgs(); 26 | if (args.length < 2) { 27 | println("Insufficient arguments. Expected: "); 28 | return; 29 | } 30 | this.port = Integer.parseInt(args[0]); 31 | // Parse libraries from comma-separated string 32 | this.libraryPrefixes = Arrays.asList(args[1].split(",")); 33 | } 34 | 35 | private boolean isLibraryNamespace(String namespace) { 36 | return libraryPrefixes.stream() 37 | .anyMatch(prefix -> namespace.startsWith(prefix)); 38 | } 39 | 40 | private int getPort() { 41 | String[] args = getScriptArgs(); 42 | if (args.length > 0) { 43 | return Integer.parseInt(args[0]); 44 | } 45 | println("No port provided. Exiting script."); 46 | return -1; 47 | } 48 | 49 | private String formatNamespaceName(String namespaceName) { 50 | if ("".equals(namespaceName)) { 51 | return "Global"; 52 | } else if ("".equals(namespaceName)) { 53 | return "External"; 54 | } 55 | return namespaceName; 56 | } 57 | 58 | private JSONArray extractClassFunctionData(Program program) { 59 | FunctionManager functionManager = program.getFunctionManager(); 60 | List classFunctionData = new ArrayList<>(); 61 | 62 | Map> namespaceFunctionData = new HashMap<>(); 63 | 64 | for (Function function : functionManager.getFunctions(true)) { 65 | Namespace namespace = function.getParentNamespace(); 66 | String namespaceName = formatNamespaceName(namespace != null ? namespace.getName() : ""); 67 | 68 | namespaceFunctionData.computeIfAbsent(namespaceName, k -> new ArrayList<>()).add(function.getName()); 69 | } 70 | 71 | for (Map.Entry> entry : namespaceFunctionData.entrySet()) { 72 | JSONObject classObject = new JSONObject(); 73 | classObject.put("ClassName", entry.getKey()); 74 | classObject.put("Functions", new JSONArray(entry.getValue())); 75 | classFunctionData.add(classObject); 76 | } 77 | 78 | return new JSONArray(classFunctionData); 79 | } 80 | 81 | private JSONObject listDefinedDataInAllSegments(Program program) { 82 | Memory memory = program.getMemory(); 83 | Listing listing = program.getListing(); 84 | Map dataStructure = new HashMap<>(); 85 | 86 | for (MemoryBlock block : memory.getBlocks()) { 87 | Address start = block.getStart(); 88 | Address end = block.getEnd(); 89 | String name = block.getName(); 90 | 91 | JSONObject segmentData = new JSONObject(); 92 | segmentData.put("start", start.toString()); 93 | segmentData.put("end", end.toString()); 94 | JSONArray dataArray = new JSONArray(); 95 | 96 | DataIterator dataIterator = listing.getDefinedData(start, true); 97 | while (dataIterator.hasNext()) { 98 | Data data = dataIterator.next(); 99 | if (!block.contains(data.getAddress())) { 100 | continue; 101 | } 102 | 103 | String label = data.getLabel(); 104 | String value = data.getDefaultValueRepresentation(); 105 | String address = data.getAddress().toString(); 106 | 107 | JSONObject dataEntry = new JSONObject(); 108 | dataEntry.put("label", label != null ? label : "Unnamed"); 109 | dataEntry.put("value", value); 110 | dataEntry.put("address", address); 111 | dataArray.put(dataEntry); 112 | } 113 | 114 | segmentData.put("data", dataArray); 115 | dataStructure.put(name, segmentData); 116 | } 117 | 118 | return new JSONObject(dataStructure); 119 | } 120 | 121 | private JSONArray listFunctionsAndNamespaces(Program program) { 122 | DecompInterface decompInterface = new DecompInterface(); 123 | FunctionManager functionManager = program.getFunctionManager(); 124 | Map> namespaceFunctionsMap = new HashMap<>(); 125 | JSONArray jsonOutput = new JSONArray(); 126 | 127 | decompInterface.openProgram(program); 128 | 129 | // Collect functions for each namespace 130 | for (Function function : functionManager.getFunctions(true)) { 131 | Namespace namespace = function.getParentNamespace(); 132 | String namespaceName = formatNamespaceName(namespace != null ? namespace.getName() : ""); 133 | 134 | // Skip decompilation if namespace is a library 135 | if (isLibraryNamespace(namespaceName)) { 136 | // Add basic function info without decompilation 137 | JSONObject jsonEntry = new JSONObject(); 138 | jsonEntry.put("FunctionName", function.getName()); 139 | jsonEntry.put("ClassName", namespaceName); 140 | jsonEntry.put("DecompiledCode", ""); // Empty string for library functions 141 | jsonOutput.put(jsonEntry); 142 | continue; 143 | } 144 | 145 | // Add function to namespace map for non-library functions 146 | namespaceFunctionsMap.computeIfAbsent(namespaceName, k -> new ArrayList<>()).add(function); 147 | } 148 | 149 | // Decompile non-library functions 150 | for (Map.Entry> entry : namespaceFunctionsMap.entrySet()) { 151 | String namespace = entry.getKey(); 152 | List functions = entry.getValue(); 153 | 154 | for (Function function : functions) { 155 | var decompiledFunction = decompInterface.decompileFunction(function, 0, new ConsoleTaskMonitor()); 156 | if (decompiledFunction.decompileCompleted()) { 157 | String decompiledCode = decompiledFunction.getDecompiledFunction().getC(); 158 | 159 | JSONObject jsonEntry = new JSONObject(); 160 | jsonEntry.put("FunctionName", function.getName()); 161 | jsonEntry.put("ClassName", namespace); 162 | jsonEntry.put("DecompiledCode", decompiledCode); 163 | jsonOutput.put(jsonEntry); 164 | } 165 | } 166 | } 167 | 168 | decompInterface.dispose(); 169 | return jsonOutput; 170 | } 171 | 172 | private JSONArray extractStrings(Program program) { 173 | Memory memory = program.getMemory(); 174 | Listing listing = program.getListing(); 175 | JSONArray stringsArray = new JSONArray(); 176 | 177 | for (MemoryBlock block : memory.getBlocks()) { 178 | if (!block.isInitialized()) continue; 179 | 180 | DataIterator dataIterator = listing.getDefinedData(block.getStart(), true); 181 | while (dataIterator.hasNext()) { 182 | Data data = dataIterator.next(); 183 | if (!block.contains(data.getAddress())) continue; 184 | 185 | // Check if the data type is a string 186 | DataType dataType = data.getDataType(); 187 | if (dataType instanceof StringDataType) { 188 | String value = data.getDefaultValueRepresentation(); // Retrieves the string value 189 | // Only include strings of length 5 or more 190 | if (value.length() >= 5) { 191 | JSONObject stringObj = new JSONObject(); 192 | stringObj.put("address", data.getAddress().toString()); 193 | stringObj.put("value", value); 194 | stringObj.put("segment", block.getName()); 195 | stringObj.put("label", data.getLabel() != null ? data.getLabel() : ""); 196 | stringsArray.put(stringObj); 197 | } 198 | } 199 | } 200 | } 201 | return stringsArray; 202 | } 203 | 204 | private void sendDataViaSocket(JSONArray classData, JSONObject machoData, JSONArray functionData) { 205 | int port = getPort(); 206 | if (port == -1) return; 207 | 208 | try (Socket socket = new Socket("localhost", port); 209 | PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) { 210 | 211 | out.println("CONNECTED"); 212 | println("Beginning analysis..."); 213 | 214 | // Send class data 215 | out.println(classData.toString(4)); 216 | out.println("END_CLASS_DATA"); 217 | 218 | // Send Macho data 219 | out.println(machoData.toString(4)); 220 | out.println("END_MACHO_DATA"); 221 | 222 | // Send function decompilation data 223 | out.println(functionData.toString(4)); 224 | out.println("END_DATA"); 225 | 226 | // Send string data 227 | JSONArray stringData = extractStrings(currentProgram); 228 | out.println(stringData.toString(4)); 229 | out.println("END_STRING_DATA"); 230 | 231 | } catch (IOException e) { 232 | printerr("Error sending data via socket: " + e.getMessage()); 233 | } 234 | } 235 | 236 | @Override 237 | public void run() throws Exception { 238 | System.err.println("Running DumpCombinedData script"); 239 | parseArgs(); 240 | 241 | if (port == -1) { 242 | return; 243 | } 244 | 245 | // Perform heartbeat check first 246 | try (Socket heartbeatSocket = new Socket("localhost", port); 247 | PrintWriter heartbeatOut = new PrintWriter(heartbeatSocket.getOutputStream(), true)) { 248 | 249 | heartbeatOut.println("HEARTBEAT"); 250 | println("Heartbeat sent successfully, proceeding with analysis..."); 251 | } catch (IOException e) { 252 | printerr("Failed to establish initial connection: " + e.getMessage()); 253 | return; 254 | } 255 | 256 | // Continue with analysis after successful heartbeat 257 | JSONArray classData = extractClassFunctionData(currentProgram); 258 | JSONObject machoData = listDefinedDataInAllSegments(currentProgram); 259 | JSONArray functionData = listFunctionsAndNamespaces(currentProgram); 260 | 261 | sendDataViaSocket(classData, machoData, functionData); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Malimite logo](https://github.com/LaurieWired/Malimite/blob/main/media/malimite_logo.png) 2 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) 3 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/LaurieWired/Malimite)](https://github.com/LaurieWired/Malimite/releases) 4 | [![GitHub stars](https://img.shields.io/github/stars/LaurieWired/Malimite)](https://github.com/LaurieWired/Malimite/stargazers) 5 | [![GitHub forks](https://img.shields.io/github/forks/LaurieWired/Malimite)](https://github.com/LaurieWired/Malimite/network/members) 6 | [![GitHub contributors](https://img.shields.io/github/contributors/LaurieWired/Malimite)](https://github.com/LaurieWired/Malimite/graphs/contributors) 7 | [![Follow @lauriewired](https://img.shields.io/twitter/follow/lauriewired?style=social)](https://twitter.com/lauriewired) 8 | 9 | # Description 10 | 11 | Malimite is an iOS and macOS decompiler designed to help researchers analyze and decode IPA files and Application Bundles. 12 | 13 | Built on top of Ghidra decompilation to offer direct support for Swift, Objective-C, and Apple resources. 14 | 15 | 16 | ![Malimite Features](https://github.com/LaurieWired/Malimite/blob/main/media/malimite_features_github.png) 17 | 18 | 19 | # Features 20 | - Multi-Platform 21 | - Mac, Windows, Linux 22 | - Direct support for IPA and bundle files 23 | - Auto decodes iOS resources 24 | - Avoids lib code decompilation 25 | - Reconstructs Swift classes 26 | - **Built-in LLM method translation** 27 | 28 | 29 | # Installation 30 | 31 | A precompiled JAR file is provided in the [Releases Page](https://github.com/LaurieWired/Malimite/releases/) 32 | 33 | For full Installation steps consult the Wiki. 34 | 35 | # Usage + Prequisites 36 | 37 | ### Check out the **[Wiki](https://github.com/LaurieWired/Malimite/wiki)** for more details. 38 | 39 | 40 | # Contribute 41 | - Make a pull request 42 | - Add an Example to our Wiki 43 | - Report an error/issue 44 | - Suggest an improvement 45 | - Share with others or give a star! 46 | 47 | Your contributions are greatly appreciated and will help make Malimite an even more powerful and versatile tool for the iOS and macOS Reverse Engineering community. 48 | 49 | # License 50 | 51 | Malimite is licensed under the Apache 2.0 License. See the [LICENSE](https://www.apache.org/licenses/LICENSE-2.0) file for more information. 52 | -------------------------------------------------------------------------------- /lib/ghidra.jar: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:44d801c909561c832d734cba3b69bce8d604faf12781c8cdff5e8e04c2481fd1 3 | size 316202328 4 | -------------------------------------------------------------------------------- /media/gatekeeper_allow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurieWired/Malimite/89d274f699896beaa3d38325c6b8952fc7a9b25d/media/gatekeeper_allow.png -------------------------------------------------------------------------------- /media/malimite_dependencies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurieWired/Malimite/89d274f699896beaa3d38325c6b8952fc7a9b25d/media/malimite_dependencies.png -------------------------------------------------------------------------------- /media/malimite_features_github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurieWired/Malimite/89d274f699896beaa3d38325c6b8952fc7a9b25d/media/malimite_features_github.png -------------------------------------------------------------------------------- /media/malimite_gatekeeper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurieWired/Malimite/89d274f699896beaa3d38325c6b8952fc7a9b25d/media/malimite_gatekeeper.png -------------------------------------------------------------------------------- /media/malimite_ghidra_path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurieWired/Malimite/89d274f699896beaa3d38325c6b8952fc7a9b25d/media/malimite_ghidra_path.png -------------------------------------------------------------------------------- /media/malimite_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurieWired/Malimite/89d274f699896beaa3d38325c6b8952fc7a9b25d/media/malimite_logo.png -------------------------------------------------------------------------------- /media/malimite_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurieWired/Malimite/89d274f699896beaa3d38325c6b8952fc7a9b25d/media/malimite_open.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.lauriewired.malimite 5 | malimite 6 | jar 7 | 1.0-SNAPSHOT 8 | malimite 9 | http://maven.apache.org 10 | 11 | 12 | UTF-8 13 | 11 14 | 11 15 | 16 | 17 | 18 | 19 | junit 20 | junit 21 | 3.8.1 22 | test 23 | 24 | 25 | com.googlecode.plist 26 | dd-plist 27 | 1.27 28 | 29 | 30 | com.google.code.gson 31 | gson 32 | 2.10.1 33 | 34 | 35 | org.xerial 36 | sqlite-jdbc 37 | 3.44.1.0 38 | 39 | 40 | org.json 41 | json 42 | 20231013 43 | 44 | 45 | org.antlr 46 | antlr4-runtime 47 | 4.13.1 48 | 49 | 50 | com.fifesoft 51 | rsyntaxtextarea 52 | 3.4.0 53 | 54 | 55 | com.formdev 56 | flatlaf 57 | 3.5.2 58 | 59 | 60 | org.bouncycastle 61 | bcprov-jdk15on 62 | 1.70 63 | 64 | 65 | org.bouncycastle 66 | bcpkix-jdk15on 67 | 1.70 68 | 69 | 70 | com.vladsch.flexmark 71 | flexmark-all 72 | 0.64.8 73 | 74 | 75 | ghidra 76 | ghidra 77 | 11.3.1 78 | system 79 | ${project.basedir}/lib/ghidra.jar 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | org.antlr 88 | antlr4-maven-plugin 89 | 4.13.1 90 | 91 | 92 | antlr 93 | generate-sources 94 | 95 | antlr4 96 | 97 | 98 | src/main/antlr4 99 | src/main/java/com/lauriewired/malimite/decompile/antlr 100 | 101 | -package 102 | com.lauriewired.malimite.decompile.antlr 103 | -visitor 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | org.codehaus.mojo 113 | build-helper-maven-plugin 114 | 3.3.0 115 | 116 | 117 | add-source 118 | generate-sources 119 | 120 | add-source 121 | 122 | 123 | 124 | src/main/java/com/lauriewired/malimite/decompile/antlr 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | org.apache.maven.plugins 134 | maven-compiler-plugin 135 | 3.13.0 136 | 137 | 11 138 | 11 139 | 11 140 | 141 | 142 | 143 | 144 | 145 | org.apache.maven.plugins 146 | maven-shade-plugin 147 | 3.5.0 148 | 149 | 150 | package 151 | 152 | shade 153 | 154 | 155 | 156 | 157 | *:* 158 | 159 | 160 | META-INF/*.SF 161 | META-INF/*.DSA 162 | META-INF/*.RSA 163 | 164 | 165 | 166 | 167 | 168 | com.lauriewired.malimite.Malimite 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | src/main/resources 179 | 180 | **/* 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /src/main/antlr4/CPP14Lexer.g4: -------------------------------------------------------------------------------- 1 | // $antlr-format alignTrailingComments true, columnLimit 150, maxEmptyLinesToKeep 1, reflowComments false, useTab false 2 | // $antlr-format allowShortRulesOnASingleLine true, allowShortBlocksOnASingleLine true, minEmptyLines 0, alignSemicolons ownLine 3 | // $antlr-format alignColons trailing, singleLineOverrulesHangingColon true, alignLexerCommands true, alignLabels true, alignTrailers true 4 | 5 | lexer grammar CPP14Lexer; 6 | 7 | IntegerLiteral: 8 | DecimalLiteral Integersuffix? 9 | | OctalLiteral Integersuffix? 10 | | HexadecimalLiteral Integersuffix? 11 | | BinaryLiteral Integersuffix? 12 | ; 13 | 14 | CharacterLiteral: ('u' | 'U' | 'L')? '\'' Cchar+ '\''; 15 | 16 | FloatingLiteral: 17 | Fractionalconstant Exponentpart? Floatingsuffix? 18 | | Digitsequence Exponentpart Floatingsuffix? 19 | ; 20 | 21 | StringLiteral: Encodingprefix? (Rawstring | '"' Schar* '"'); 22 | 23 | BooleanLiteral: False_ | True_; 24 | 25 | PointerLiteral: Nullptr; 26 | 27 | UserDefinedLiteral: 28 | UserDefinedIntegerLiteral 29 | | UserDefinedFloatingLiteral 30 | | UserDefinedStringLiteral 31 | | UserDefinedCharacterLiteral 32 | ; 33 | 34 | MultiLineMacro: '#' (~[\n]*? '\\' '\r'? '\n')+ ~ [\n]+ -> channel (HIDDEN); 35 | 36 | Directive: '#' ~ [\n]* -> channel (HIDDEN); 37 | /*Keywords*/ 38 | 39 | Alignas: 'alignas'; 40 | 41 | Alignof: 'alignof'; 42 | 43 | Asm: 'asm'; 44 | 45 | Auto: 'auto'; 46 | 47 | Bool: 'bool'; 48 | 49 | Break: 'break'; 50 | 51 | Case: 'case'; 52 | 53 | Catch: 'catch'; 54 | 55 | Char: 'char'; 56 | 57 | Char16: 'char16_t'; 58 | 59 | Char32: 'char32_t'; 60 | 61 | Class: 'class'; 62 | 63 | Const: 'const'; 64 | 65 | Constexpr: 'constexpr'; 66 | 67 | Const_cast: 'const_cast'; 68 | 69 | Continue: 'continue'; 70 | 71 | Decltype: 'decltype'; 72 | 73 | Default: 'default'; 74 | 75 | Delete: 'delete'; 76 | 77 | Do: 'do'; 78 | 79 | Double: 'double'; 80 | 81 | Dynamic_cast: 'dynamic_cast'; 82 | 83 | Else: 'else'; 84 | 85 | Enum: 'enum'; 86 | 87 | Explicit: 'explicit'; 88 | 89 | Export: 'export'; 90 | 91 | Extern: 'extern'; 92 | 93 | //DO NOT RENAME - PYTHON NEEDS True and False 94 | False_: 'false'; 95 | 96 | Final: 'final'; 97 | 98 | Float: 'float'; 99 | 100 | For: 'for'; 101 | 102 | Friend: 'friend'; 103 | 104 | Goto: 'goto'; 105 | 106 | If: 'if'; 107 | 108 | Inline: 'inline'; 109 | 110 | Int: 'int'; 111 | 112 | Long: 'long'; 113 | 114 | Mutable: 'mutable'; 115 | 116 | Namespace: 'namespace'; 117 | 118 | New: 'new'; 119 | 120 | Noexcept: 'noexcept'; 121 | 122 | Nullptr: 'nullptr'; 123 | 124 | Operator: 'operator'; 125 | 126 | Override: 'override'; 127 | 128 | Private: 'private'; 129 | 130 | Protected: 'protected'; 131 | 132 | Public: 'public'; 133 | 134 | Register: 'register'; 135 | 136 | Reinterpret_cast: 'reinterpret_cast'; 137 | 138 | Return: 'return'; 139 | 140 | Short: 'short'; 141 | 142 | Signed: 'signed'; 143 | 144 | Sizeof: 'sizeof'; 145 | 146 | Static: 'static'; 147 | 148 | Static_assert: 'static_assert'; 149 | 150 | Static_cast: 'static_cast'; 151 | 152 | Struct: 'struct'; 153 | 154 | Switch: 'switch'; 155 | 156 | Template: 'template'; 157 | 158 | This: 'this'; 159 | 160 | Thread_local: 'thread_local'; 161 | 162 | Throw: 'throw'; 163 | 164 | //DO NOT RENAME - PYTHON NEEDS True and False 165 | True_: 'true'; 166 | 167 | Try: 'try'; 168 | 169 | Typedef: 'typedef'; 170 | 171 | Typeid_: 'typeid'; 172 | 173 | Typename_: 'typename'; 174 | 175 | Union: 'union'; 176 | 177 | Unsigned: 'unsigned'; 178 | 179 | Using: 'using'; 180 | 181 | Virtual: 'virtual'; 182 | 183 | Void: 'void'; 184 | 185 | Volatile: 'volatile'; 186 | 187 | Wchar: 'wchar_t'; 188 | 189 | While: 'while'; 190 | /*Operators*/ 191 | 192 | LeftParen: '('; 193 | 194 | RightParen: ')'; 195 | 196 | LeftBracket: '['; 197 | 198 | RightBracket: ']'; 199 | 200 | LeftBrace: '{'; 201 | 202 | RightBrace: '}'; 203 | 204 | Plus: '+'; 205 | 206 | Minus: '-'; 207 | 208 | Star: '*'; 209 | 210 | Div: '/'; 211 | 212 | Mod: '%'; 213 | 214 | Caret: '^'; 215 | 216 | And: '&'; 217 | 218 | Or: '|'; 219 | 220 | Tilde: '~'; 221 | 222 | Not: '!' | 'not'; 223 | 224 | Assign: '='; 225 | 226 | Less: '<'; 227 | 228 | Greater: '>'; 229 | 230 | PlusAssign: '+='; 231 | 232 | MinusAssign: '-='; 233 | 234 | StarAssign: '*='; 235 | 236 | DivAssign: '/='; 237 | 238 | ModAssign: '%='; 239 | 240 | XorAssign: '^='; 241 | 242 | AndAssign: '&='; 243 | 244 | OrAssign: '|='; 245 | 246 | LeftShiftAssign: '<<='; 247 | 248 | RightShiftAssign: '>>='; 249 | 250 | Equal: '=='; 251 | 252 | NotEqual: '!='; 253 | 254 | LessEqual: '<='; 255 | 256 | GreaterEqual: '>='; 257 | 258 | AndAnd: '&&' | 'and'; 259 | 260 | OrOr: '||' | 'or'; 261 | 262 | PlusPlus: '++'; 263 | 264 | MinusMinus: '--'; 265 | 266 | Comma: ','; 267 | 268 | ArrowStar: '->*'; 269 | 270 | Arrow: '->'; 271 | 272 | Question: '?'; 273 | 274 | Colon: ':'; 275 | 276 | Doublecolon: '::'; 277 | 278 | Semi: ';'; 279 | 280 | Dot: '.'; 281 | 282 | DotStar: '.*'; 283 | 284 | Ellipsis: '...'; 285 | 286 | fragment Hexquad: HEXADECIMALDIGIT HEXADECIMALDIGIT HEXADECIMALDIGIT HEXADECIMALDIGIT; 287 | 288 | fragment Universalcharactername: '\\u' Hexquad | '\\U' Hexquad Hexquad; 289 | 290 | Identifier: 291 | /* 292 | Identifiernondigit | Identifier Identifiernondigit | Identifier DIGIT 293 | */ Identifiernondigit (Identifiernondigit | DIGIT)* 294 | ; 295 | 296 | fragment Identifiernondigit: NONDIGIT | Universalcharactername; 297 | 298 | fragment NONDIGIT: [a-zA-Z_]; 299 | 300 | fragment DIGIT: [0-9]; 301 | 302 | DecimalLiteral: NONZERODIGIT ('\''? DIGIT)*; 303 | 304 | OctalLiteral: '0' ('\''? OCTALDIGIT)*; 305 | 306 | HexadecimalLiteral: ('0x' | '0X') HEXADECIMALDIGIT ( '\''? HEXADECIMALDIGIT)*; 307 | 308 | BinaryLiteral: ('0b' | '0B') BINARYDIGIT ('\''? BINARYDIGIT)*; 309 | 310 | fragment NONZERODIGIT: [1-9]; 311 | 312 | fragment OCTALDIGIT: [0-7]; 313 | 314 | fragment HEXADECIMALDIGIT: [0-9a-fA-F]; 315 | 316 | fragment BINARYDIGIT: [01]; 317 | 318 | Integersuffix: 319 | Unsignedsuffix Longsuffix? 320 | | Unsignedsuffix Longlongsuffix? 321 | | Longsuffix Unsignedsuffix? 322 | | Longlongsuffix Unsignedsuffix? 323 | ; 324 | 325 | fragment Unsignedsuffix: [uU]; 326 | 327 | fragment Longsuffix: [lL]; 328 | 329 | fragment Longlongsuffix: 'll' | 'LL'; 330 | 331 | fragment Cchar: ~ ['\\\r\n] | Escapesequence | Universalcharactername; 332 | 333 | fragment Escapesequence: Simpleescapesequence | Octalescapesequence | Hexadecimalescapesequence; 334 | 335 | fragment Simpleescapesequence: 336 | '\\\'' 337 | | '\\"' 338 | | '\\?' 339 | | '\\\\' 340 | | '\\a' 341 | | '\\b' 342 | | '\\f' 343 | | '\\n' 344 | | '\\r' 345 | | '\\' ('\r' '\n'? | '\n') 346 | | '\\t' 347 | | '\\v' 348 | ; 349 | 350 | fragment Octalescapesequence: 351 | '\\' OCTALDIGIT 352 | | '\\' OCTALDIGIT OCTALDIGIT 353 | | '\\' OCTALDIGIT OCTALDIGIT OCTALDIGIT 354 | ; 355 | 356 | fragment Hexadecimalescapesequence: '\\x' HEXADECIMALDIGIT+; 357 | 358 | fragment Fractionalconstant: Digitsequence? '.' Digitsequence | Digitsequence '.'; 359 | 360 | fragment Exponentpart: 'e' SIGN? Digitsequence | 'E' SIGN? Digitsequence; 361 | 362 | fragment SIGN: [+-]; 363 | 364 | fragment Digitsequence: DIGIT ('\''? DIGIT)*; 365 | 366 | fragment Floatingsuffix: [flFL]; 367 | 368 | fragment Encodingprefix: 'u8' | 'u' | 'U' | 'L'; 369 | 370 | fragment Schar: ~ ["\\\r\n] | Escapesequence | Universalcharactername; 371 | 372 | fragment Rawstring: 'R"' ( '\\' ["()] | ~[\r\n (])*? '(' ~[)]*? ')' ( '\\' ["()] | ~[\r\n "])*? '"'; 373 | 374 | UserDefinedIntegerLiteral: 375 | DecimalLiteral Udsuffix 376 | | OctalLiteral Udsuffix 377 | | HexadecimalLiteral Udsuffix 378 | | BinaryLiteral Udsuffix 379 | ; 380 | 381 | UserDefinedFloatingLiteral: 382 | Fractionalconstant Exponentpart? Udsuffix 383 | | Digitsequence Exponentpart Udsuffix 384 | ; 385 | 386 | UserDefinedStringLiteral: StringLiteral Udsuffix; 387 | 388 | UserDefinedCharacterLiteral: CharacterLiteral Udsuffix; 389 | 390 | fragment Udsuffix: Identifier; 391 | 392 | Whitespace: [ \t]+ -> skip; 393 | 394 | Newline: ('\r' '\n'? | '\n') -> skip; 395 | 396 | BlockComment: '/*' .*? '*/' -> skip; 397 | 398 | LineComment: '//' ~ [\r\n]* -> skip; -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/configuration/Config.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.configuration; 2 | 3 | import java.io.*; 4 | import java.util.Properties; 5 | import java.util.logging.Logger; 6 | import java.util.logging.Level; 7 | import java.util.List; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | 11 | import com.lauriewired.malimite.security.KeyEncryption; 12 | 13 | public class Config { 14 | private static final Logger LOGGER = Logger.getLogger(Config.class.getName()); 15 | private static final String CONFIG_FILE = "malimite.properties"; 16 | private static final String GHIDRA_PATH_KEY = "ghidra.path"; 17 | private static final String THEME_KEY = "app.theme"; 18 | private static final String OS_TYPE_KEY = "os.type"; 19 | private static final String ENCRYPTED_OPENAI_API_KEY = "openai.api.key.encrypted"; 20 | private static final String ENCRYPTED_CLAUDE_API_KEY = "claude.api.key.encrypted"; 21 | private static final String LOCAL_MODEL_URL = "local.model.url"; 22 | private static final String PROJECTS_LIST_KEY = "projects.list"; 23 | private static final String ADDED_LIBRARIES_KEY = "libraries.added"; 24 | private static final String REMOVED_LIBRARIES_KEY = "libraries.removed"; 25 | 26 | private String osType; 27 | private String ghidraPath; 28 | private String theme; 29 | private Properties properties; 30 | private String configDirectory; 31 | private String encryptedOpenAIApiKey; 32 | private String encryptedClaudeApiKey; 33 | 34 | public Config() { 35 | this.osType = System.getProperty("os.name").toLowerCase(); 36 | this.properties = new Properties(); 37 | this.configDirectory = "."; 38 | loadConfig(); 39 | 40 | properties.setProperty(OS_TYPE_KEY, this.osType); 41 | 42 | if (this.theme == null) { 43 | this.theme = "dark"; 44 | properties.setProperty(THEME_KEY, this.theme); 45 | saveConfig(); 46 | } 47 | } 48 | 49 | public void loadConfig() { 50 | File configFile = new File(CONFIG_FILE); 51 | if (configFile.exists()) { 52 | try (FileInputStream fis = new FileInputStream(configFile)) { 53 | properties.load(fis); 54 | this.configDirectory = configFile.getParent() != null ? configFile.getParent() : "."; 55 | this.ghidraPath = properties.getProperty(GHIDRA_PATH_KEY); 56 | this.theme = properties.getProperty(THEME_KEY); 57 | this.osType = properties.getProperty(OS_TYPE_KEY, System.getProperty("os.name").toLowerCase()); 58 | this.encryptedOpenAIApiKey = properties.getProperty(ENCRYPTED_OPENAI_API_KEY); 59 | this.encryptedClaudeApiKey = properties.getProperty(ENCRYPTED_CLAUDE_API_KEY); 60 | } catch (IOException e) { 61 | LOGGER.log(Level.WARNING, "Failed to load configuration file", e); 62 | } 63 | } 64 | } 65 | 66 | public void saveConfig() { 67 | try (FileOutputStream fos = new FileOutputStream(CONFIG_FILE)) { 68 | properties.store(fos, "Malimite Configuration"); 69 | } catch (IOException e) { 70 | LOGGER.log(Level.WARNING, "Failed to save configuration file", e); 71 | } 72 | } 73 | 74 | public String getGhidraPath() { 75 | return ghidraPath; 76 | } 77 | 78 | public void setGhidraPath(String ghidraPath) { 79 | this.ghidraPath = ghidraPath; 80 | properties.setProperty(GHIDRA_PATH_KEY, ghidraPath); 81 | saveConfig(); 82 | } 83 | 84 | public boolean isWindows() { 85 | return osType.contains("win"); 86 | } 87 | 88 | public boolean isMac() { 89 | return osType.contains("mac"); 90 | } 91 | 92 | public boolean isUnix() { 93 | return osType.contains("nix") || osType.contains("nux") || osType.contains("aix"); 94 | } 95 | 96 | public String getTheme() { 97 | return theme; 98 | } 99 | 100 | public void setTheme(String theme) { 101 | this.theme = theme; 102 | properties.setProperty(THEME_KEY, theme); 103 | saveConfig(); 104 | } 105 | 106 | public String getConfigDirectory() { 107 | return configDirectory; 108 | } 109 | 110 | public String getOpenAIApiKey() { 111 | return encryptedOpenAIApiKey; 112 | } 113 | 114 | public void setOpenAIApiKey(String key) { 115 | this.encryptedOpenAIApiKey = KeyEncryption.encrypt(key); 116 | properties.setProperty(ENCRYPTED_OPENAI_API_KEY, this.encryptedOpenAIApiKey); 117 | saveConfig(); 118 | } 119 | 120 | public String getClaudeApiKey() { 121 | return encryptedClaudeApiKey; 122 | } 123 | 124 | public void setClaudeApiKey(String key) { 125 | this.encryptedClaudeApiKey = KeyEncryption.encrypt(key); 126 | properties.setProperty(ENCRYPTED_CLAUDE_API_KEY, this.encryptedClaudeApiKey); 127 | saveConfig(); 128 | } 129 | 130 | public String getLocalModelUrl() { 131 | return properties.getProperty(LOCAL_MODEL_URL, "http://localhost:1234/v1/chat/completions"); 132 | } 133 | 134 | public void setLocalModelUrl(String url) { 135 | properties.setProperty(LOCAL_MODEL_URL, url); 136 | saveConfig(); 137 | } 138 | 139 | public List getProjectPaths() { 140 | String projectsStr = properties.getProperty(PROJECTS_LIST_KEY, ""); 141 | if (projectsStr.isEmpty()) { 142 | return new ArrayList<>(); 143 | } 144 | return new ArrayList<>(Arrays.asList(projectsStr.split("\\|"))); 145 | } 146 | 147 | public void addProjectPath(String path) { 148 | List projects = getProjectPaths(); 149 | if (!projects.contains(path)) { 150 | projects.add(path); 151 | properties.setProperty(PROJECTS_LIST_KEY, String.join("|", projects)); 152 | saveConfig(); 153 | } 154 | } 155 | 156 | public List getAddedLibraries() { 157 | String addedStr = properties.getProperty(ADDED_LIBRARIES_KEY, ""); 158 | if (addedStr.isEmpty()) { 159 | return new ArrayList<>(); 160 | } 161 | return new ArrayList<>(Arrays.asList(addedStr.split("\\|"))); 162 | } 163 | 164 | public List getRemovedLibraries() { 165 | String removedStr = properties.getProperty(REMOVED_LIBRARIES_KEY, ""); 166 | if (removedStr.isEmpty()) { 167 | return new ArrayList<>(); 168 | } 169 | return new ArrayList<>(Arrays.asList(removedStr.split("\\|"))); 170 | } 171 | 172 | public void addLibrary(String library) { 173 | List addedLibraries = getAddedLibraries(); 174 | List removedLibraries = getRemovedLibraries(); 175 | 176 | if (removedLibraries.contains(library)) { 177 | // If it was previously removed, just remove it from the removed list 178 | removedLibraries.remove(library); 179 | properties.setProperty(REMOVED_LIBRARIES_KEY, String.join("|", removedLibraries)); 180 | } else if (!LibraryDefinitions.getDefaultLibraries().contains(library) 181 | && !addedLibraries.contains(library)) { 182 | // Only add to added list if it's not a default library and not already added 183 | addedLibraries.add(library); 184 | properties.setProperty(ADDED_LIBRARIES_KEY, String.join("|", addedLibraries)); 185 | } 186 | saveConfig(); 187 | } 188 | 189 | public void removeLibrary(String library) { 190 | List addedLibraries = getAddedLibraries(); 191 | List removedLibraries = getRemovedLibraries(); 192 | 193 | if (addedLibraries.contains(library)) { 194 | // If it was previously added, just remove it from the added list 195 | addedLibraries.remove(library); 196 | properties.setProperty(ADDED_LIBRARIES_KEY, String.join("|", addedLibraries)); 197 | } else if (LibraryDefinitions.getDefaultLibraries().contains(library) 198 | && !removedLibraries.contains(library)) { 199 | // Only add to removed list if it's a default library and not already removed 200 | removedLibraries.add(library); 201 | properties.setProperty(REMOVED_LIBRARIES_KEY, String.join("|", removedLibraries)); 202 | } 203 | saveConfig(); 204 | } 205 | 206 | public void clearLibraryConfigurations() { 207 | properties.remove(ADDED_LIBRARIES_KEY); 208 | properties.remove(REMOVED_LIBRARIES_KEY); 209 | saveConfig(); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/configuration/LibraryDefinitions.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.configuration; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.Set; 6 | import java.util.HashSet; 7 | import java.util.Collections; 8 | import java.util.ArrayList; 9 | 10 | public class LibraryDefinitions { 11 | // Common iOS frameworks to avoid decompiling 12 | private static final List DEFAULT_LIBRARIES = Arrays.asList( 13 | "UIKit", 14 | "Foundation", 15 | "CoreData", 16 | "CoreGraphics", 17 | "CoreLocation", 18 | "AVFoundation", 19 | "WebKit", 20 | "Security", 21 | "NetworkExtension", 22 | "SystemConfiguration", 23 | "CoreBluetooth", 24 | "CoreMotion", 25 | "Photos", 26 | "Contacts", 27 | "HealthKit", 28 | "HomeKit", 29 | "MapKit", 30 | "MessageUI", 31 | "StoreKit", 32 | "UserNotifications", 33 | "SwiftStandardLibrary", 34 | "SwiftUI", 35 | "Combine", 36 | "CoreFoundation", 37 | "QuartzCore", 38 | "CFNetwork", 39 | "CoreImage", 40 | "Metal", 41 | "SceneKit", 42 | "ARKit", 43 | "SpriteKit", 44 | "GameKit", 45 | "BackgroundTasks", 46 | "CloudKit", 47 | "FileProvider", 48 | "CoreText", 49 | "Vision", 50 | "TextKit", 51 | "CoreML", 52 | "NaturalLanguage", 53 | "AppTrackingTransparency", 54 | "AuthenticationServices", 55 | "Intents", 56 | "CallKit", 57 | "MediaPlayer", 58 | "PassKit" 59 | ); 60 | 61 | 62 | public static List getDefaultLibraries() { 63 | return DEFAULT_LIBRARIES; 64 | } 65 | 66 | public static List getActiveLibraries(Config config) { 67 | Set activeLibraries = new HashSet<>(DEFAULT_LIBRARIES); 68 | 69 | // Remove any libraries that the user has explicitly removed 70 | activeLibraries.removeAll(config.getRemovedLibraries()); 71 | 72 | // Add any custom libraries the user has added 73 | activeLibraries.addAll(config.getAddedLibraries()); 74 | 75 | // Convert back to sorted list for consistent ordering 76 | List sortedLibraries = new ArrayList<>(activeLibraries); 77 | Collections.sort(sortedLibraries); 78 | return sortedLibraries; 79 | } 80 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/configuration/Project.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.configuration; 2 | 3 | import com.lauriewired.malimite.files.Macho; 4 | 5 | public class Project { 6 | private String fileName; 7 | private String filePath; 8 | private String fileType; 9 | private boolean isMachO; 10 | private Macho machoInfo; 11 | private String bundleIdentifier; 12 | private boolean isSwift; 13 | private long size; 14 | 15 | public Project() { 16 | } 17 | 18 | // Basic file info getters/setters 19 | public String getFileName() { return fileName; } 20 | public void setFileName(String fileName) { this.fileName = fileName; } 21 | 22 | public String getFilePath() { return filePath; } 23 | public void setFilePath(String filePath) { this.filePath = filePath; } 24 | 25 | public String getFileType() { return fileType; } 26 | public void setFileType(String fileType) { this.fileType = fileType; } 27 | 28 | public boolean isMachO() { return isMachO; } 29 | public void setIsMachO(boolean isMachO) { this.isMachO = isMachO; } 30 | 31 | public Macho getMachoInfo() { return machoInfo; } 32 | public void setMachoInfo(Macho machoInfo) { this.machoInfo = machoInfo; } 33 | 34 | public String getBundleIdentifier() { return bundleIdentifier; } 35 | public void setBundleIdentifier(String bundleIdentifier) { this.bundleIdentifier = bundleIdentifier; } 36 | 37 | public boolean isSwift() { return isSwift; } 38 | public void setIsSwift(boolean isSwift) { this.isSwift = isSwift; } 39 | 40 | public long getSize() { return size; } 41 | public void setSize(long size) { this.size = size; } 42 | 43 | public String generateInfoString() { 44 | StringBuilder info = new StringBuilder(""); 45 | info.append("

File Analysis Report

"); 46 | info.append("

File: ").append(fileName).append("

"); 47 | info.append("

Size: ").append(size / 1024).append(" KB

"); 48 | info.append("

Type: ").append(fileType).append("

"); 49 | 50 | if (bundleIdentifier != null && !bundleIdentifier.isEmpty()) { 51 | info.append("

Bundle ID: ").append(bundleIdentifier).append("

"); 52 | } 53 | 54 | if (isMachO && machoInfo != null) { 55 | info.append("

Mach-O Analysis

"); 56 | info.append("

Language: ").append(this.isSwift ? "Swift" : "Objective-C").append("

"); 57 | if (machoInfo.isUniversalBinary()) { 58 | info.append("

Universal Binary: Yes

"); 59 | info.append("

Architectures:

    "); 60 | for (Macho.Architecture arch : machoInfo.getArchitectures()) { 61 | info.append("
  • ").append(arch.toString()).append("
  • "); 62 | } 63 | info.append("
"); 64 | } 65 | } 66 | 67 | info.append(""); 68 | return info.toString(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/decompile/DemangleSwift.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.decompile; 2 | 3 | import java.util.logging.Logger; 4 | import java.util.logging.Level; 5 | 6 | public class DemangleSwift { 7 | private static final Logger LOGGER = Logger.getLogger(DemangleSwift.class.getName()); 8 | 9 | public static class DemangledName { 10 | public final String className; 11 | public final String fullMethodName; 12 | 13 | public DemangledName(String className, String fullMethodName) { 14 | this.className = className; 15 | this.fullMethodName = fullMethodName; 16 | } 17 | } 18 | 19 | public static DemangledName demangleSwiftName(String mangledName) { 20 | if (mangledName == null || !mangledName.startsWith("_$s")) { 21 | return null; // Not a valid Swift mangled name 22 | } 23 | 24 | try { 25 | // Drop the _$s prefix 26 | String remaining = mangledName.substring(3); 27 | 28 | // Extract the class name 29 | int classNameLength = extractNumber(remaining); 30 | String className = remaining.substring(String.valueOf(classNameLength).length(), 31 | String.valueOf(classNameLength).length() + classNameLength); 32 | 33 | // Move past the class name 34 | remaining = remaining.substring(String.valueOf(classNameLength).length() + classNameLength); 35 | 36 | // Extract the method name 37 | StringBuilder methodNameBuilder = new StringBuilder(); 38 | 39 | while (!remaining.isEmpty()) { 40 | // Skip leading zeros and extract the next number 41 | int numberIndex = findNextNumberIndex(remaining); 42 | if (numberIndex == -1) break; // No more numbers, exit loop 43 | 44 | String remainingAfterNumber = remaining.substring(numberIndex); 45 | int length = extractNumber(remainingAfterNumber); 46 | 47 | // Skip the number itself in the string 48 | int numberLength = String.valueOf(length).length(); 49 | String segment = remainingAfterNumber.substring(numberLength, numberLength + length); 50 | methodNameBuilder.append(segment); 51 | 52 | // Update the remaining string 53 | remaining = remainingAfterNumber.substring(numberLength + length); 54 | } 55 | 56 | String methodName = methodNameBuilder.toString(); 57 | return new DemangledName(className, methodName); 58 | } catch (Exception e) { 59 | System.err.println("Failed to demangle Swift name: " + mangledName); 60 | e.printStackTrace(); 61 | } 62 | 63 | return null; 64 | } 65 | 66 | private static int findNextNumberIndex(String str) { 67 | for (int i = 0; i < str.length(); i++) { 68 | if (Character.isDigit(str.charAt(i)) && str.charAt(i) != '0') { 69 | return i; // Return index of the first non-zero digit 70 | } 71 | } 72 | return -1; // No valid number found 73 | } 74 | 75 | private static int extractNumber(String str) { 76 | StringBuilder numberBuilder = new StringBuilder(); 77 | boolean leadingZeroSkipped = false; 78 | 79 | for (int i = 0; i < str.length(); i++) { 80 | char c = str.charAt(i); 81 | if (Character.isDigit(c)) { 82 | if (c != '0' || leadingZeroSkipped) { 83 | numberBuilder.append(c); 84 | leadingZeroSkipped = true; 85 | } 86 | } else { 87 | break; // Stop when we reach a non-digit 88 | } 89 | } 90 | return numberBuilder.length() > 0 ? Integer.parseInt(numberBuilder.toString()) : 0; 91 | } 92 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/decompile/DynamicDecompile.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.decompile; 2 | 3 | import com.lauriewired.malimite.ui.AnalysisWindow; 4 | import com.lauriewired.malimite.configuration.Config; 5 | import com.lauriewired.malimite.database.SQLiteDBHandler; 6 | import com.lauriewired.malimite.files.Macho; 7 | import com.lauriewired.malimite.utils.FileProcessing; 8 | import java.io.File; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.logging.Logger; 12 | import javax.swing.tree.DefaultMutableTreeNode; 13 | import javax.swing.tree.DefaultTreeModel; 14 | import java.util.HashMap; 15 | import java.util.Enumeration; 16 | import javax.swing.JTree; 17 | import javax.swing.tree.TreePath; 18 | import javax.swing.JDialog; 19 | import javax.swing.JPanel; 20 | import javax.swing.JProgressBar; 21 | import javax.swing.JLabel; 22 | import javax.swing.JTextArea; 23 | import javax.swing.JScrollPane; 24 | import javax.swing.BorderFactory; 25 | import javax.swing.SwingUtilities; 26 | import javax.swing.JFrame; 27 | import javax.swing.JOptionPane; 28 | import java.awt.BorderLayout; 29 | import java.util.logging.Level; 30 | import javax.swing.SwingWorker; 31 | 32 | public class DynamicDecompile { 33 | private static final Logger LOGGER = Logger.getLogger(DynamicDecompile.class.getName()); 34 | 35 | public static void decompileFile(String filePath, String projectDirectoryPath, String fullFilePath, Config config, 36 | SQLiteDBHandler dbHandler, String infoPlistExecutableName, DefaultTreeModel treeModel, JTree fileTree) { 37 | 38 | // Create and configure progress dialog 39 | JDialog progressDialog = new JDialog((JFrame)SwingUtilities.getWindowAncestor(fileTree), "Analyzing File", false); 40 | progressDialog.setAlwaysOnTop(true); 41 | progressDialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); 42 | JPanel panel = new JPanel(new BorderLayout(10, 10)); 43 | panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); 44 | 45 | // Add progress components 46 | JProgressBar progressBar = new JProgressBar(); 47 | progressBar.setIndeterminate(true); 48 | JLabel statusLabel = new JLabel("Analyzing file..."); 49 | 50 | // Add console output area 51 | JTextArea consoleOutput = new JTextArea(10, 50); 52 | consoleOutput.setEditable(false); 53 | JScrollPane scrollPane = new JScrollPane(consoleOutput); 54 | 55 | // Add components to panel 56 | panel.add(statusLabel, BorderLayout.NORTH); 57 | panel.add(progressBar, BorderLayout.CENTER); 58 | panel.add(scrollPane, BorderLayout.SOUTH); 59 | 60 | progressDialog.add(panel); 61 | progressDialog.pack(); 62 | progressDialog.setLocationRelativeTo(fileTree); 63 | 64 | // Create SwingWorker for background processing 65 | SwingWorker worker = new SwingWorker() { 66 | @Override 67 | protected Void doInBackground() throws Exception { 68 | try { 69 | publish("Starting analysis of: " + fullFilePath); 70 | LOGGER.info("Decompiling: " + fullFilePath); 71 | 72 | File file = new File(fullFilePath); 73 | String fileName = file.getName(); 74 | 75 | publish("Opening project..."); 76 | FileProcessing.openProject( 77 | filePath, 78 | projectDirectoryPath, 79 | fileName, 80 | config.getConfigDirectory(), 81 | true 82 | ); 83 | 84 | String extractedMachoPath = projectDirectoryPath + File.separator + fileName; 85 | publish("Creating Macho object..."); 86 | Macho targetMacho = new Macho(extractedMachoPath, projectDirectoryPath, fileName); 87 | 88 | publish("Starting Ghidra analysis..."); 89 | GhidraProject ghidraProject = new GhidraProject( 90 | infoPlistExecutableName, 91 | extractedMachoPath, 92 | config, 93 | dbHandler, 94 | // Pass console output callback 95 | message -> publish(message) 96 | ); 97 | 98 | ghidraProject.decompileMacho(extractedMachoPath, projectDirectoryPath, targetMacho, true); 99 | 100 | return null; 101 | } catch (Exception e) { 102 | publish("Error: " + e.getMessage()); 103 | throw e; 104 | } 105 | } 106 | 107 | @Override 108 | protected void process(List chunks) { 109 | // Update console with new messages 110 | for (String message : chunks) { 111 | consoleOutput.append(message + "\n"); 112 | consoleOutput.setCaretPosition(consoleOutput.getDocument().getLength()); 113 | } 114 | } 115 | 116 | @Override 117 | protected void done() { 118 | try { 119 | get(); // Check for exceptions 120 | 121 | // Update tree on success 122 | SwingUtilities.invokeLater(() -> { 123 | DefaultMutableTreeNode decompiledNode = addDecompiledNode(treeModel); 124 | populateDecompiledNode(decompiledNode, dbHandler, infoPlistExecutableName); 125 | AnalysisWindow.populateMachoStringsPanel(); 126 | 127 | // Expand nodes 128 | TreePath decompiledPath = new TreePath(decompiledNode.getPath()); 129 | fileTree.expandPath(decompiledPath); 130 | Enumeration decompiledChildren = decompiledNode.children(); 131 | while (decompiledChildren.hasMoreElements()) { 132 | DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) decompiledChildren.nextElement(); 133 | TreePath childPath = new TreePath(childNode.getPath()); 134 | fileTree.expandPath(childPath); 135 | } 136 | }); 137 | 138 | } catch (Exception e) { 139 | LOGGER.log(Level.SEVERE, "Error during decompilation", e); 140 | JOptionPane.showMessageDialog(progressDialog, 141 | "Error during decompilation: " + e.getMessage(), 142 | "Decompilation Error", 143 | JOptionPane.ERROR_MESSAGE); 144 | } finally { 145 | progressDialog.dispose(); 146 | } 147 | } 148 | }; 149 | 150 | // Start the worker and show dialog 151 | worker.execute(); 152 | progressDialog.setVisible(true); 153 | } 154 | 155 | private static DefaultMutableTreeNode addDecompiledNode(DefaultTreeModel treeModel) { 156 | // Get the invisible root node 157 | DefaultMutableTreeNode rootNode = (DefaultMutableTreeNode) treeModel.getRoot(); 158 | 159 | // Check if the "Decompiled" node already exists 160 | Enumeration children = rootNode.children(); 161 | while (children.hasMoreElements()) { 162 | DefaultMutableTreeNode child = (DefaultMutableTreeNode) children.nextElement(); 163 | if ("Decompiled".equals(child.getUserObject().toString())) { 164 | return child; // Return the existing "Decompiled" node 165 | } 166 | } 167 | 168 | // If not found, create a new "Decompiled" node 169 | DefaultMutableTreeNode decompiledNode = new DefaultMutableTreeNode("Decompiled"); 170 | rootNode.add(decompiledNode); 171 | treeModel.reload(rootNode); 172 | return decompiledNode; 173 | } 174 | 175 | private static void populateDecompiledNode(DefaultMutableTreeNode decompiledNode, SQLiteDBHandler dbHandler, String infoPlistExecutableName) { 176 | Map> classesAndFunctions = dbHandler.getAllClassesAndFunctions(); 177 | Map executableNodes = new HashMap<>(); 178 | 179 | for (Map.Entry> entry : classesAndFunctions.entrySet()) { 180 | String className = entry.getKey(); 181 | List functions = entry.getValue(); 182 | 183 | // Retrieve the executable name for the current class 184 | String executableName = dbHandler.getExecutableNameForClass(className); 185 | 186 | // Check if the class belongs to the specified executable 187 | if (!infoPlistExecutableName.equals(executableName)) { 188 | // Get or create the node for this executable 189 | DefaultMutableTreeNode executableNode = executableNodes.computeIfAbsent(executableName, k -> { 190 | DefaultMutableTreeNode node = new DefaultMutableTreeNode(executableName); 191 | decompiledNode.add(node); 192 | return node; 193 | }); 194 | 195 | // Create a node for the class 196 | DefaultMutableTreeNode classNode = new DefaultMutableTreeNode(className); 197 | 198 | // Add function nodes under the class node 199 | for (String function : functions) { 200 | classNode.add(new DefaultMutableTreeNode(function)); 201 | } 202 | 203 | // Add the class node under the executable node 204 | executableNode.add(classNode); 205 | } 206 | } 207 | } 208 | 209 | public static void repopulateDecompiledNode(DefaultTreeModel treeModel, SQLiteDBHandler dbHandler, String infoPlistExecutableName) { 210 | DefaultMutableTreeNode decompiledNode = addDecompiledNode(treeModel); 211 | decompiledNode.removeAllChildren(); // Clear existing children before repopulating 212 | populateDecompiledNode(decompiledNode, dbHandler, infoPlistExecutableName); 213 | 214 | // Only add the "Decompiled" node if it has children 215 | if (decompiledNode.getChildCount() > 0) { 216 | treeModel.reload(decompiledNode); 217 | } else { 218 | // Remove the "Decompiled" node if it has no children 219 | ((DefaultMutableTreeNode) treeModel.getRoot()).remove(decompiledNode); 220 | treeModel.reload(); 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/decompile/SyntaxParser.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.decompile; 2 | 3 | import org.antlr.v4.runtime.*; 4 | import org.antlr.v4.runtime.tree.*; 5 | 6 | import com.lauriewired.malimite.database.SQLiteDBHandler; 7 | import com.lauriewired.malimite.decompile.antlr.CPP14ParserBaseVisitor; 8 | import com.lauriewired.malimite.decompile.antlr.CPP14Lexer; 9 | import com.lauriewired.malimite.decompile.antlr.CPP14Parser; 10 | 11 | import java.util.logging.Level; 12 | import java.util.logging.Logger; 13 | 14 | public class SyntaxParser { 15 | private CPP14Lexer lexer = new CPP14Lexer(null); 16 | private CPP14Parser parser = new CPP14Parser(new CommonTokenStream(lexer)); 17 | private static final Logger LOGGER = Logger.getLogger(SyntaxParser.class.getName()); 18 | private SQLiteDBHandler dbHandler; 19 | private String currentFunction; 20 | private String currentClass; 21 | private String formattedCode; 22 | private String executableName; 23 | 24 | public SyntaxParser(SQLiteDBHandler dbHandler, String executableName) { 25 | this.dbHandler = dbHandler; 26 | this.executableName = executableName; 27 | lexer.removeErrorListeners(); 28 | parser.removeErrorListeners(); 29 | } 30 | 31 | public void setContext(String functionName, String className) { 32 | this.currentFunction = functionName; 33 | this.currentClass = className; 34 | } 35 | 36 | public String parseAndFormatCode(String code) { 37 | try { 38 | CharStream input = CharStreams.fromString(code); 39 | lexer.setInputStream(input); 40 | 41 | CommonTokenStream tokens = new CommonTokenStream(lexer); 42 | parser.setTokenStream(tokens); 43 | 44 | // Temporarily return unformatted code while testing performance 45 | return code; 46 | } catch (Exception e) { 47 | LOGGER.log(Level.SEVERE, "Error parsing code", e); 48 | return code; 49 | } 50 | } 51 | 52 | public void collectCrossReferences(String formattedCode) { 53 | if (dbHandler == null || currentFunction == null || currentClass == null) { 54 | LOGGER.warning("Cannot collect cross-references: missing context or database handler"); 55 | return; 56 | } 57 | 58 | this.formattedCode = formattedCode; 59 | try { 60 | CharStream input = CharStreams.fromString(formattedCode); 61 | lexer.setInputStream(input); 62 | 63 | CommonTokenStream tokens = new CommonTokenStream(lexer); 64 | parser.setTokenStream(tokens); 65 | 66 | ParseTree tree = parser.translationUnit(); 67 | if (tree == null) { 68 | LOGGER.warning("Failed to parse code for cross-references"); 69 | return; 70 | } 71 | 72 | new CrossReferenceVisitor().visit(tree); 73 | } catch (Exception e) { 74 | LOGGER.log(Level.SEVERE, "Error collecting cross-references", e); 75 | } 76 | } 77 | 78 | private class CrossReferenceVisitor extends CPP14ParserBaseVisitor { 79 | @Override 80 | public Void visitPostfixExpression(CPP14Parser.PostfixExpressionContext ctx) { 81 | // Only handle function calls 82 | if (ctx.getChildCount() >= 2 && ctx.getChild(1).getText().equals("(")) { 83 | String calledFunction = ctx.getChild(0).getText(); 84 | String calledClass = null; 85 | 86 | // Extract the class name if it's a method call (contains ::) 87 | if (calledFunction.contains("::")) { 88 | String[] parts = calledFunction.split("::"); 89 | calledClass = parts[0]; 90 | calledFunction = parts[1]; 91 | } 92 | 93 | // Calculate adjusted line number 94 | int actualLine = calculateActualLineNumber(ctx.getStart().getLine()); 95 | 96 | // Store the function reference with adjusted line number 97 | dbHandler.insertFunctionReference( 98 | dbHandler.GetTransaction(), 99 | currentFunction, 100 | currentClass, 101 | calledFunction, 102 | calledClass != null ? calledClass : "Unknown", 103 | actualLine, 104 | executableName 105 | ); 106 | } 107 | return visitChildren(ctx); 108 | } 109 | 110 | @Override 111 | public Void visitDeclarationStatement(CPP14Parser.DeclarationStatementContext ctx) { 112 | if (ctx.blockDeclaration() != null && 113 | ctx.blockDeclaration().simpleDeclaration() != null) { 114 | 115 | CPP14Parser.SimpleDeclarationContext simpleDecl = 116 | ctx.blockDeclaration().simpleDeclaration(); 117 | 118 | // Add null check for declSpecifierSeq 119 | String variableType = ""; 120 | if (simpleDecl.declSpecifierSeq() != null) { 121 | variableType = simpleDecl.declSpecifierSeq().getText(); 122 | } 123 | 124 | // Process each declarator in the declaration 125 | if (simpleDecl.initDeclaratorList() != null) { 126 | for (CPP14Parser.InitDeclaratorContext initDecl : 127 | simpleDecl.initDeclaratorList().initDeclarator()) { 128 | 129 | String variableName = initDecl.declarator().getText(); 130 | // Clean up variable name (remove initialization if present) 131 | if (variableName.contains("=")) { 132 | variableName = variableName.substring(0, 133 | variableName.indexOf("=")).trim(); 134 | } 135 | 136 | // Use adjusted line numbers when storing references 137 | int actualLine = calculateActualLineNumber(ctx.getStart().getLine()); 138 | 139 | // Store the type information 140 | dbHandler.insertTypeInformation( 141 | dbHandler.GetTransaction(), 142 | variableName, 143 | variableType, 144 | currentFunction, 145 | currentClass, 146 | actualLine, 147 | executableName 148 | ); 149 | 150 | // Store initial local variable reference 151 | dbHandler.insertLocalVariableReference( 152 | dbHandler.GetTransaction(), 153 | variableName, 154 | currentFunction, 155 | currentClass, 156 | actualLine, 157 | executableName 158 | ); 159 | } 160 | } 161 | } 162 | return visitChildren(ctx); 163 | } 164 | 165 | @Override 166 | public Void visitIdExpression(CPP14Parser.IdExpressionContext ctx) { 167 | String identifier = ctx.getText(); 168 | 169 | // Use adjusted line numbers 170 | int actualLine = calculateActualLineNumber(ctx.getStart().getLine()); 171 | 172 | // Handle class references (contains ::) 173 | if (identifier.contains("::")) { 174 | String[] parts = identifier.split("::"); 175 | String referencedClass = parts[0]; 176 | 177 | // Store class usage reference 178 | dbHandler.insertFunctionReference( 179 | dbHandler.GetTransaction(), 180 | currentFunction, 181 | currentClass, 182 | null, // No specific function 183 | referencedClass, 184 | actualLine, 185 | executableName 186 | ); 187 | } 188 | // Handle local variable references 189 | else { 190 | // Check if this identifier is in a function call context 191 | if (!isPartOfFunctionCall(ctx)) { 192 | dbHandler.insertLocalVariableReference( 193 | dbHandler.GetTransaction(), 194 | identifier, 195 | currentFunction, 196 | currentClass, 197 | actualLine, 198 | executableName 199 | ); 200 | } 201 | } 202 | 203 | return visitChildren(ctx); 204 | } 205 | 206 | private boolean isPartOfFunctionCall(CPP14Parser.IdExpressionContext ctx) { 207 | // Check if this identifier is immediately followed by ( 208 | ParseTree parent = ctx.getParent(); 209 | while (parent != null) { 210 | if (parent instanceof CPP14Parser.PostfixExpressionContext) { 211 | CPP14Parser.PostfixExpressionContext postfix = 212 | (CPP14Parser.PostfixExpressionContext) parent; 213 | // Check if this is a function call 214 | return postfix.getChildCount() >= 2 && 215 | postfix.getChild(1).getText().equals("("); 216 | } 217 | parent = parent.getParent(); 218 | } 219 | return false; 220 | } 221 | 222 | private int calculateActualLineNumber(int parsedLineNumber) { 223 | // Count actual code lines up to the parsed line number 224 | String[] lines = formattedCode.split("\n", parsedLineNumber + 1); 225 | int actualLineNumber = 0; 226 | 227 | for (int i = 0; i < Math.min(lines.length, parsedLineNumber); i++) { 228 | actualLineNumber++; // Every line, including empty ones and comments, counts 229 | String line = lines[i].trim(); 230 | 231 | // For multi-line comments, add the additional lines 232 | if (line.contains("/*")) { 233 | int currentLine = i; 234 | while (currentLine < lines.length && !lines[currentLine].contains("*/")) { 235 | if (currentLine != i) { // Don't double-count the first line 236 | actualLineNumber++; 237 | } 238 | currentLine++; 239 | } 240 | if (currentLine < lines.length && lines[currentLine].contains("*/")) { 241 | if (currentLine != i) { // Don't double-count if comment ends on same line 242 | actualLineNumber++; 243 | } 244 | } 245 | i = currentLine; 246 | } 247 | } 248 | 249 | return actualLineNumber; 250 | } 251 | } 252 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/decompile/antlr/CPP14Lexer.tokens: -------------------------------------------------------------------------------- 1 | IntegerLiteral=1 2 | CharacterLiteral=2 3 | FloatingLiteral=3 4 | StringLiteral=4 5 | BooleanLiteral=5 6 | PointerLiteral=6 7 | UserDefinedLiteral=7 8 | MultiLineMacro=8 9 | Directive=9 10 | Alignas=10 11 | Alignof=11 12 | Asm=12 13 | Auto=13 14 | Bool=14 15 | Break=15 16 | Case=16 17 | Catch=17 18 | Char=18 19 | Char16=19 20 | Char32=20 21 | Class=21 22 | Const=22 23 | Constexpr=23 24 | Const_cast=24 25 | Continue=25 26 | Decltype=26 27 | Default=27 28 | Delete=28 29 | Do=29 30 | Double=30 31 | Dynamic_cast=31 32 | Else=32 33 | Enum=33 34 | Explicit=34 35 | Export=35 36 | Extern=36 37 | False_=37 38 | Final=38 39 | Float=39 40 | For=40 41 | Friend=41 42 | Goto=42 43 | If=43 44 | Inline=44 45 | Int=45 46 | Long=46 47 | Mutable=47 48 | Namespace=48 49 | New=49 50 | Noexcept=50 51 | Nullptr=51 52 | Operator=52 53 | Override=53 54 | Private=54 55 | Protected=55 56 | Public=56 57 | Register=57 58 | Reinterpret_cast=58 59 | Return=59 60 | Short=60 61 | Signed=61 62 | Sizeof=62 63 | Static=63 64 | Static_assert=64 65 | Static_cast=65 66 | Struct=66 67 | Switch=67 68 | Template=68 69 | This=69 70 | Thread_local=70 71 | Throw=71 72 | True_=72 73 | Try=73 74 | Typedef=74 75 | Typeid_=75 76 | Typename_=76 77 | Union=77 78 | Unsigned=78 79 | Using=79 80 | Virtual=80 81 | Void=81 82 | Volatile=82 83 | Wchar=83 84 | While=84 85 | LeftParen=85 86 | RightParen=86 87 | LeftBracket=87 88 | RightBracket=88 89 | LeftBrace=89 90 | RightBrace=90 91 | Plus=91 92 | Minus=92 93 | Star=93 94 | Div=94 95 | Mod=95 96 | Caret=96 97 | And=97 98 | Or=98 99 | Tilde=99 100 | Not=100 101 | Assign=101 102 | Less=102 103 | Greater=103 104 | PlusAssign=104 105 | MinusAssign=105 106 | StarAssign=106 107 | DivAssign=107 108 | ModAssign=108 109 | XorAssign=109 110 | AndAssign=110 111 | OrAssign=111 112 | LeftShiftAssign=112 113 | RightShiftAssign=113 114 | Equal=114 115 | NotEqual=115 116 | LessEqual=116 117 | GreaterEqual=117 118 | AndAnd=118 119 | OrOr=119 120 | PlusPlus=120 121 | MinusMinus=121 122 | Comma=122 123 | ArrowStar=123 124 | Arrow=124 125 | Question=125 126 | Colon=126 127 | Doublecolon=127 128 | Semi=128 129 | Dot=129 130 | DotStar=130 131 | Ellipsis=131 132 | Identifier=132 133 | DecimalLiteral=133 134 | OctalLiteral=134 135 | HexadecimalLiteral=135 136 | BinaryLiteral=136 137 | Integersuffix=137 138 | UserDefinedIntegerLiteral=138 139 | UserDefinedFloatingLiteral=139 140 | UserDefinedStringLiteral=140 141 | UserDefinedCharacterLiteral=141 142 | Whitespace=142 143 | Newline=143 144 | BlockComment=144 145 | LineComment=145 146 | 'alignas'=10 147 | 'alignof'=11 148 | 'asm'=12 149 | 'auto'=13 150 | 'bool'=14 151 | 'break'=15 152 | 'case'=16 153 | 'catch'=17 154 | 'char'=18 155 | 'char16_t'=19 156 | 'char32_t'=20 157 | 'class'=21 158 | 'const'=22 159 | 'constexpr'=23 160 | 'const_cast'=24 161 | 'continue'=25 162 | 'decltype'=26 163 | 'default'=27 164 | 'delete'=28 165 | 'do'=29 166 | 'double'=30 167 | 'dynamic_cast'=31 168 | 'else'=32 169 | 'enum'=33 170 | 'explicit'=34 171 | 'export'=35 172 | 'extern'=36 173 | 'false'=37 174 | 'final'=38 175 | 'float'=39 176 | 'for'=40 177 | 'friend'=41 178 | 'goto'=42 179 | 'if'=43 180 | 'inline'=44 181 | 'int'=45 182 | 'long'=46 183 | 'mutable'=47 184 | 'namespace'=48 185 | 'new'=49 186 | 'noexcept'=50 187 | 'nullptr'=51 188 | 'operator'=52 189 | 'override'=53 190 | 'private'=54 191 | 'protected'=55 192 | 'public'=56 193 | 'register'=57 194 | 'reinterpret_cast'=58 195 | 'return'=59 196 | 'short'=60 197 | 'signed'=61 198 | 'sizeof'=62 199 | 'static'=63 200 | 'static_assert'=64 201 | 'static_cast'=65 202 | 'struct'=66 203 | 'switch'=67 204 | 'template'=68 205 | 'this'=69 206 | 'thread_local'=70 207 | 'throw'=71 208 | 'true'=72 209 | 'try'=73 210 | 'typedef'=74 211 | 'typeid'=75 212 | 'typename'=76 213 | 'union'=77 214 | 'unsigned'=78 215 | 'using'=79 216 | 'virtual'=80 217 | 'void'=81 218 | 'volatile'=82 219 | 'wchar_t'=83 220 | 'while'=84 221 | '('=85 222 | ')'=86 223 | '['=87 224 | ']'=88 225 | '{'=89 226 | '}'=90 227 | '+'=91 228 | '-'=92 229 | '*'=93 230 | '/'=94 231 | '%'=95 232 | '^'=96 233 | '&'=97 234 | '|'=98 235 | '~'=99 236 | '='=101 237 | '<'=102 238 | '>'=103 239 | '+='=104 240 | '-='=105 241 | '*='=106 242 | '/='=107 243 | '%='=108 244 | '^='=109 245 | '&='=110 246 | '|='=111 247 | '<<='=112 248 | '>>='=113 249 | '=='=114 250 | '!='=115 251 | '<='=116 252 | '>='=117 253 | '++'=120 254 | '--'=121 255 | ','=122 256 | '->*'=123 257 | '->'=124 258 | '?'=125 259 | ':'=126 260 | '::'=127 261 | ';'=128 262 | '.'=129 263 | '.*'=130 264 | '...'=131 265 | -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/decompile/antlr/CPP14Parser.tokens: -------------------------------------------------------------------------------- 1 | IntegerLiteral=1 2 | CharacterLiteral=2 3 | FloatingLiteral=3 4 | StringLiteral=4 5 | BooleanLiteral=5 6 | PointerLiteral=6 7 | UserDefinedLiteral=7 8 | MultiLineMacro=8 9 | Directive=9 10 | Alignas=10 11 | Alignof=11 12 | Asm=12 13 | Auto=13 14 | Bool=14 15 | Break=15 16 | Case=16 17 | Catch=17 18 | Char=18 19 | Char16=19 20 | Char32=20 21 | Class=21 22 | Const=22 23 | Constexpr=23 24 | Const_cast=24 25 | Continue=25 26 | Decltype=26 27 | Default=27 28 | Delete=28 29 | Do=29 30 | Double=30 31 | Dynamic_cast=31 32 | Else=32 33 | Enum=33 34 | Explicit=34 35 | Export=35 36 | Extern=36 37 | False_=37 38 | Final=38 39 | Float=39 40 | For=40 41 | Friend=41 42 | Goto=42 43 | If=43 44 | Inline=44 45 | Int=45 46 | Long=46 47 | Mutable=47 48 | Namespace=48 49 | New=49 50 | Noexcept=50 51 | Nullptr=51 52 | Operator=52 53 | Override=53 54 | Private=54 55 | Protected=55 56 | Public=56 57 | Register=57 58 | Reinterpret_cast=58 59 | Return=59 60 | Short=60 61 | Signed=61 62 | Sizeof=62 63 | Static=63 64 | Static_assert=64 65 | Static_cast=65 66 | Struct=66 67 | Switch=67 68 | Template=68 69 | This=69 70 | Thread_local=70 71 | Throw=71 72 | True_=72 73 | Try=73 74 | Typedef=74 75 | Typeid_=75 76 | Typename_=76 77 | Union=77 78 | Unsigned=78 79 | Using=79 80 | Virtual=80 81 | Void=81 82 | Volatile=82 83 | Wchar=83 84 | While=84 85 | LeftParen=85 86 | RightParen=86 87 | LeftBracket=87 88 | RightBracket=88 89 | LeftBrace=89 90 | RightBrace=90 91 | Plus=91 92 | Minus=92 93 | Star=93 94 | Div=94 95 | Mod=95 96 | Caret=96 97 | And=97 98 | Or=98 99 | Tilde=99 100 | Not=100 101 | Assign=101 102 | Less=102 103 | Greater=103 104 | PlusAssign=104 105 | MinusAssign=105 106 | StarAssign=106 107 | DivAssign=107 108 | ModAssign=108 109 | XorAssign=109 110 | AndAssign=110 111 | OrAssign=111 112 | LeftShiftAssign=112 113 | RightShiftAssign=113 114 | Equal=114 115 | NotEqual=115 116 | LessEqual=116 117 | GreaterEqual=117 118 | AndAnd=118 119 | OrOr=119 120 | PlusPlus=120 121 | MinusMinus=121 122 | Comma=122 123 | ArrowStar=123 124 | Arrow=124 125 | Question=125 126 | Colon=126 127 | Doublecolon=127 128 | Semi=128 129 | Dot=129 130 | DotStar=130 131 | Ellipsis=131 132 | Identifier=132 133 | DecimalLiteral=133 134 | OctalLiteral=134 135 | HexadecimalLiteral=135 136 | BinaryLiteral=136 137 | Integersuffix=137 138 | UserDefinedIntegerLiteral=138 139 | UserDefinedFloatingLiteral=139 140 | UserDefinedStringLiteral=140 141 | UserDefinedCharacterLiteral=141 142 | Whitespace=142 143 | Newline=143 144 | BlockComment=144 145 | LineComment=145 146 | 'alignas'=10 147 | 'alignof'=11 148 | 'asm'=12 149 | 'auto'=13 150 | 'bool'=14 151 | 'break'=15 152 | 'case'=16 153 | 'catch'=17 154 | 'char'=18 155 | 'char16_t'=19 156 | 'char32_t'=20 157 | 'class'=21 158 | 'const'=22 159 | 'constexpr'=23 160 | 'const_cast'=24 161 | 'continue'=25 162 | 'decltype'=26 163 | 'default'=27 164 | 'delete'=28 165 | 'do'=29 166 | 'double'=30 167 | 'dynamic_cast'=31 168 | 'else'=32 169 | 'enum'=33 170 | 'explicit'=34 171 | 'export'=35 172 | 'extern'=36 173 | 'false'=37 174 | 'final'=38 175 | 'float'=39 176 | 'for'=40 177 | 'friend'=41 178 | 'goto'=42 179 | 'if'=43 180 | 'inline'=44 181 | 'int'=45 182 | 'long'=46 183 | 'mutable'=47 184 | 'namespace'=48 185 | 'new'=49 186 | 'noexcept'=50 187 | 'nullptr'=51 188 | 'operator'=52 189 | 'override'=53 190 | 'private'=54 191 | 'protected'=55 192 | 'public'=56 193 | 'register'=57 194 | 'reinterpret_cast'=58 195 | 'return'=59 196 | 'short'=60 197 | 'signed'=61 198 | 'sizeof'=62 199 | 'static'=63 200 | 'static_assert'=64 201 | 'static_cast'=65 202 | 'struct'=66 203 | 'switch'=67 204 | 'template'=68 205 | 'this'=69 206 | 'thread_local'=70 207 | 'throw'=71 208 | 'true'=72 209 | 'try'=73 210 | 'typedef'=74 211 | 'typeid'=75 212 | 'typename'=76 213 | 'union'=77 214 | 'unsigned'=78 215 | 'using'=79 216 | 'virtual'=80 217 | 'void'=81 218 | 'volatile'=82 219 | 'wchar_t'=83 220 | 'while'=84 221 | '('=85 222 | ')'=86 223 | '['=87 224 | ']'=88 225 | '{'=89 226 | '}'=90 227 | '+'=91 228 | '-'=92 229 | '*'=93 230 | '/'=94 231 | '%'=95 232 | '^'=96 233 | '&'=97 234 | '|'=98 235 | '~'=99 236 | '='=101 237 | '<'=102 238 | '>'=103 239 | '+='=104 240 | '-='=105 241 | '*='=106 242 | '/='=107 243 | '%='=108 244 | '^='=109 245 | '&='=110 246 | '|='=111 247 | '<<='=112 248 | '>>='=113 249 | '=='=114 250 | '!='=115 251 | '<='=116 252 | '>='=117 253 | '++'=120 254 | '--'=121 255 | ','=122 256 | '->*'=123 257 | '->'=124 258 | '?'=125 259 | ':'=126 260 | '::'=127 261 | ';'=128 262 | '.'=129 263 | '.*'=130 264 | '...'=131 265 | -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/decompile/antlr/CPP14ParserBase.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.decompile.antlr; 2 | 3 | import org.antlr.v4.runtime.Parser; 4 | import org.antlr.v4.runtime.TokenStream; 5 | 6 | public abstract class CPP14ParserBase extends Parser { 7 | public CPP14ParserBase(TokenStream input) { 8 | super(input); 9 | } 10 | 11 | // Stub implementation for IsPureSpecifierAllowed 12 | public boolean IsPureSpecifierAllowed() { 13 | return true; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/files/InfoPlist.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.files; 2 | 3 | import java.util.Map; 4 | import javax.swing.tree.DefaultMutableTreeNode; 5 | 6 | import com.lauriewired.malimite.utils.NodeOperations; 7 | import com.lauriewired.malimite.utils.PlistUtils; 8 | import com.lauriewired.malimite.utils.FileProcessing; 9 | import com.dd.plist.NSDictionary; 10 | import com.dd.plist.NSObject; 11 | import com.dd.plist.PropertyListParser; 12 | import java.io.File; 13 | import java.nio.file.Files; 14 | 15 | public class InfoPlist { 16 | private String infoPlistBundleExecutable; 17 | private String bundleIdentifier; 18 | 19 | public InfoPlist(DefaultMutableTreeNode infoPlistNode, String filePath, Map fileEntriesMap) { 20 | try { 21 | String infoPlistPath = NodeOperations.buildFullPathFromNode(infoPlistNode); 22 | byte[] plistData; 23 | 24 | File file = new File(filePath); 25 | if (FileProcessing.isArchiveFile(file)) { 26 | // Handle archive files (IPA, ZIP, etc.) 27 | plistData = FileProcessing.readContentFromZip(filePath, fileEntriesMap.get(infoPlistPath)); 28 | } else { 29 | // Handle directories and .app bundles 30 | String directPath = fileEntriesMap.get(infoPlistPath); 31 | plistData = Files.readAllBytes(new File(directPath).toPath()); 32 | } 33 | 34 | if (PlistUtils.isBinaryPlist(plistData)) { 35 | // Handle binary plist 36 | NSObject plist = PropertyListParser.parse(plistData); 37 | infoPlistBundleExecutable = extractCFBundleExecutable(plist); 38 | bundleIdentifier = extractCFBundleIdentifier(plist); 39 | } else { 40 | // Handle XML plist 41 | String plistContent = new String(plistData); 42 | NSObject plist = PropertyListParser.parse(plistContent.getBytes()); 43 | infoPlistBundleExecutable = extractCFBundleExecutable(plist); 44 | bundleIdentifier = extractCFBundleIdentifier(plist); 45 | } 46 | } catch (Exception e) { 47 | e.printStackTrace(); 48 | } 49 | } 50 | 51 | public InfoPlist() { 52 | this.infoPlistBundleExecutable = "unknown"; 53 | this.bundleIdentifier = "unknown"; 54 | } 55 | 56 | public static InfoPlist createEmpty() { 57 | return new InfoPlist(); 58 | } 59 | 60 | /* 61 | * Takes in a binary or XML Info.plist and returns the CFBundleExecutable value 62 | */ 63 | public static String extractCFBundleExecutable(NSObject plist) { 64 | String infoPlistBundleExecutable = ""; 65 | 66 | if (plist instanceof NSDictionary) { 67 | NSDictionary dict = (NSDictionary) plist; 68 | String executableName = dict.objectForKey("CFBundleExecutable").toString(); 69 | infoPlistBundleExecutable = executableName; 70 | } 71 | 72 | return infoPlistBundleExecutable; 73 | } 74 | 75 | /* 76 | * Takes in a binary or XML Info.plist and returns the CFBundleIdentifier value 77 | */ 78 | public static String extractCFBundleIdentifier(NSObject plist) { 79 | String bundleIdentifier = ""; 80 | 81 | if (plist instanceof NSDictionary) { 82 | NSDictionary dict = (NSDictionary) plist; 83 | String identifier = dict.objectForKey("CFBundleIdentifier").toString(); 84 | bundleIdentifier = identifier; 85 | } 86 | 87 | return bundleIdentifier; 88 | } 89 | 90 | public String getExecutableName() { 91 | return this.infoPlistBundleExecutable; 92 | } 93 | 94 | public String getBundleIdentifier() { 95 | return this.bundleIdentifier; 96 | } 97 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/files/Macho.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.files; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | import java.io.IOException; 6 | import java.io.RandomAccessFile; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.logging.Logger; 10 | import java.util.logging.Level; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.nio.charset.StandardCharsets; 14 | 15 | public class Macho { 16 | private static final Logger LOGGER = Logger.getLogger(Macho.class.getName()); 17 | 18 | // Mach-O Magic Numbers 19 | private static final int UNIVERSAL_MAGIC = 0xcafebabe; 20 | private static final int UNIVERSAL_CIGAM = 0xbebafeca; 21 | 22 | private List cpuTypes; 23 | private List cpuSubTypes; 24 | private List offsets; 25 | private List sizes; 26 | private boolean isUniversal; 27 | private String machoExecutablePath; 28 | private String outputDirectoryPath; 29 | private String machoExecutableName; 30 | private boolean isSwift = false; 31 | 32 | public Macho(String machoExecutablePath, String outputDirectoryPath, String machoExecutableName) { 33 | this.isUniversal = false; 34 | this.cpuTypes = new ArrayList<>(); 35 | this.cpuSubTypes = new ArrayList<>(); 36 | this.offsets = new ArrayList<>(); 37 | this.sizes = new ArrayList<>(); 38 | this.machoExecutablePath = machoExecutablePath; 39 | this.outputDirectoryPath = outputDirectoryPath; 40 | this.machoExecutableName = machoExecutableName; 41 | 42 | processMacho(); 43 | } 44 | 45 | public void processUniversalMacho(String selectedArchitecture) { 46 | extractMachoArchitecture(selectedArchitecture); 47 | 48 | // We do not care about the original macho anymore 49 | // This will effectively reset the instance variables for the extracted macho 50 | processMacho(); 51 | } 52 | 53 | 54 | private void extractMachoArchitecture(String selectedArchitecture) { 55 | for (int i = 0; i < cpuTypes.size(); i++) { 56 | String arch = getArchitectureName(cpuTypes.get(i)); 57 | String fullArchitecture = generateFullArchitectureString(arch, cpuTypes.get(i), cpuSubTypes.get(i)); 58 | 59 | if (fullArchitecture.equals(selectedArchitecture)) { 60 | String tempFileName = machoExecutableName + "_extracted.macho"; 61 | try { 62 | extractSlice(machoExecutablePath, tempFileName, offsets.get(i), sizes.get(i)); 63 | LOGGER.info("Extracted " + arch + " slice to " + tempFileName); 64 | 65 | replaceOldMachoWithNew(tempFileName); 66 | } catch (IOException e) { 67 | LOGGER.log(Level.SEVERE, "Error extracting Mach-O slice", e); 68 | } 69 | break; 70 | } 71 | } 72 | } 73 | 74 | private void extractSlice(String inputFilePath, String outputFileName, long offset, long size) throws IOException { 75 | // Construct the full path for the output file 76 | String outputPath = outputDirectoryPath + File.separator + outputFileName; 77 | 78 | try (RandomAccessFile inputFile = new RandomAccessFile(inputFilePath, "r"); 79 | FileOutputStream outputFile = new FileOutputStream(outputPath)) { 80 | 81 | inputFile.seek(offset); 82 | byte[] buffer = new byte[8192]; 83 | long remaining = size; 84 | 85 | while (remaining > 0) { 86 | int bytesRead = inputFile.read(buffer, 0, (int) Math.min(buffer.length, remaining)); 87 | if (bytesRead == -1) break; 88 | 89 | outputFile.write(buffer, 0, bytesRead); 90 | remaining -= bytesRead; 91 | } 92 | } 93 | } 94 | 95 | private void replaceOldMachoWithNew(String tempFileName) throws IOException { 96 | File oldMacho = new File(machoExecutablePath); 97 | File extractedMacho = new File(outputDirectoryPath + File.separator + tempFileName); 98 | File newMacho = new File(machoExecutablePath); 99 | 100 | if (oldMacho.delete()) { 101 | if (!extractedMacho.renameTo(newMacho)) { 102 | throw new IOException("Failed to rename extracted Mach-O file."); 103 | } 104 | LOGGER.info("Replaced old Mach-O file with the extracted one."); 105 | } else { 106 | throw new IOException("Failed to delete old Mach-O file."); 107 | } 108 | } 109 | 110 | /* 111 | * Reads in a Mach-O file and sets instance variables based on type and architecture 112 | */ 113 | private void processMacho() { 114 | File file = new File(this.machoExecutablePath); 115 | 116 | try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { 117 | int magic = raf.readInt(); 118 | if (magic == UNIVERSAL_MAGIC || magic == UNIVERSAL_CIGAM) { 119 | this.isUniversal = true; 120 | LOGGER.info("Detected Universal binary with architectures:"); 121 | 122 | boolean reverseByteOrder = (magic == UNIVERSAL_CIGAM); 123 | int archCount = reverseByteOrder ? Integer.reverseBytes(raf.readInt()) : raf.readInt(); 124 | for (int i = 0; i < archCount; i++) { 125 | raf.seek(8L + i * 20L); 126 | int cpuType = reverseByteOrder ? Integer.reverseBytes(raf.readInt()) : raf.readInt(); 127 | int cpuSubType = reverseByteOrder ? Integer.reverseBytes(raf.readInt()) : raf.readInt(); 128 | long offset = reverseByteOrder ? Integer.reverseBytes(raf.readInt()) : raf.readInt(); 129 | long size = reverseByteOrder ? Integer.reverseBytes(raf.readInt()) : raf.readInt(); 130 | 131 | cpuTypes.add(cpuType); 132 | cpuSubTypes.add(cpuSubType); 133 | offsets.add(offset); 134 | sizes.add(size); 135 | } 136 | } else { 137 | this.isUniversal = false; 138 | LOGGER.info("This is not a Universal binary."); 139 | } 140 | 141 | // After processing the Mach-O headers, check for Swift 142 | detectSwift(file); 143 | } catch (IOException e) { 144 | LOGGER.log(Level.SEVERE, "Error reading file", e); 145 | } 146 | } 147 | 148 | private void detectSwift(File file) { 149 | try { 150 | // Read the file content as bytes 151 | byte[] content = Files.readAllBytes(file.toPath()); 152 | String stringContent = new String(content, StandardCharsets.UTF_8); 153 | 154 | // Look for common Swift indicators in the binary 155 | isSwift = stringContent.contains("Swift Runtime") || 156 | stringContent.contains("SwiftCore") || 157 | stringContent.contains("_swift_") || 158 | stringContent.contains("_$s"); // Swift name mangling prefix 159 | 160 | LOGGER.info("Binary detected as: " + (isSwift ? "Swift" : "Objective-C")); 161 | } catch (IOException e) { 162 | LOGGER.log(Level.WARNING, "Error detecting Swift/Objective-C", e); 163 | isSwift = false; // Default to Objective-C if detection fails 164 | } 165 | } 166 | 167 | public static class Architecture { 168 | private String name; 169 | private int cpuType; 170 | private int cpuSubType; 171 | 172 | public Architecture(String name, int cpuType, int cpuSubType) { 173 | this.name = name; 174 | this.cpuType = cpuType; 175 | this.cpuSubType = cpuSubType; 176 | } 177 | 178 | @Override 179 | public String toString() { 180 | return name + " (CPU Type: " + cpuType + ", SubType: " + cpuSubType + ")"; 181 | } 182 | 183 | // Getters 184 | public String getName() { return name; } 185 | public int getCpuType() { return cpuType; } 186 | public int getCpuSubType() { return cpuSubType; } 187 | } 188 | 189 | private String getArchitectureName(int cpuType) { 190 | switch (cpuType) { 191 | case 0x00000007: return "Intel x86"; 192 | case 0x01000007: return "Intel x86_64"; 193 | case 0x0000000C: return "ARM"; 194 | case 0x0100000C: return "ARM64"; 195 | default: return "Unknown"; 196 | } 197 | } 198 | 199 | public List getArchitectures() { 200 | List architectures = new ArrayList<>(); 201 | for (int i = 0; i < cpuTypes.size(); i++) { 202 | architectures.add(new Architecture( 203 | getArchitectureName(cpuTypes.get(i)), 204 | cpuTypes.get(i), 205 | cpuSubTypes.get(i) 206 | )); 207 | } 208 | return architectures; 209 | } 210 | 211 | public List getArchitectureStrings() { 212 | List architectureStrings = new ArrayList<>(); 213 | 214 | for (int i = 0; i < cpuTypes.size(); i++) { 215 | int cpuType = cpuTypes.get(i); 216 | int cpuSubType = cpuSubTypes.get(i); 217 | String arch = getArchitectureName(cpuType); 218 | 219 | String fullArchitecture = generateFullArchitectureString(arch, cpuType, cpuSubType); 220 | architectureStrings.add(fullArchitecture); 221 | } 222 | 223 | return architectureStrings; 224 | } 225 | 226 | public void printArchitectures() { 227 | String arch = ""; 228 | String fullArchitecture = ""; 229 | 230 | for (int i = 0; i < cpuTypes.size(); i++) { 231 | int cpuType = cpuTypes.get(i); 232 | int cpuSubType = cpuSubTypes.get(i); 233 | arch = getArchitectureName(cpuType); 234 | 235 | fullArchitecture = generateFullArchitectureString(arch, cpuType, cpuSubType); 236 | LOGGER.info(fullArchitecture); 237 | } 238 | } 239 | 240 | private String generateFullArchitectureString(String arch, int cpuType, int cpuSubType) { 241 | return arch + " (CPU Type: " + cpuType + ", SubType: " + cpuSubType + ")"; 242 | } 243 | 244 | public List getCpuTypes() { 245 | return cpuTypes; 246 | } 247 | 248 | public List getCpuSubTypes() { 249 | return cpuSubTypes; 250 | } 251 | 252 | public boolean isUniversalBinary() { 253 | return this.isUniversal; 254 | } 255 | 256 | public String getMachoExecutableName() { 257 | return this.machoExecutableName; 258 | } 259 | 260 | public boolean isSwift() { 261 | return isSwift; 262 | } 263 | 264 | public long getSize() { 265 | File machoFile = new File(this.machoExecutablePath); 266 | return machoFile.length(); 267 | } 268 | 269 | public Macho() { 270 | this.isUniversal = false; 271 | this.cpuTypes = new ArrayList<>(); 272 | this.cpuSubTypes = new ArrayList<>(); 273 | this.offsets = new ArrayList<>(); 274 | this.sizes = new ArrayList<>(); 275 | this.machoExecutablePath = ""; 276 | this.outputDirectoryPath = ""; 277 | this.machoExecutableName = "unknown"; 278 | this.isSwift = false; 279 | } 280 | 281 | public static Macho createEmpty() { 282 | return new Macho(); 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/files/MobileProvision.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.files; 2 | 3 | import org.bouncycastle.cms.CMSSignedData; 4 | import org.bouncycastle.cms.CMSProcessableByteArray; 5 | import com.dd.plist.NSObject; 6 | import com.dd.plist.PropertyListParser; 7 | import com.lauriewired.malimite.utils.PlistUtils; 8 | import java.util.logging.Logger; 9 | import java.util.logging.Level; 10 | 11 | public class MobileProvision { 12 | private static final Logger LOGGER = Logger.getLogger(MobileProvision.class.getName()); 13 | 14 | public static String extractEmbeddedXML(byte[] provisionData) throws Exception { 15 | LOGGER.info("Attempting to extract embedded XML from provision data"); 16 | try { 17 | CMSSignedData signedData = new CMSSignedData(new CMSProcessableByteArray(provisionData), provisionData); 18 | String fullContent = new String((byte[]) signedData.getSignedContent().getContent()); 19 | 20 | // Find the start and end of the XML plist content 21 | int plistStart = fullContent.indexOf("") + "".length(); 23 | 24 | if (plistStart == -1 || plistEnd == -1) { 25 | LOGGER.severe("Failed to locate XML plist content in embedded.mobileprovision"); 26 | throw new Exception("Failed to locate XML plist content in embedded.mobileprovision"); 27 | } 28 | 29 | LOGGER.info("Successfully extracted XML content from provision data"); 30 | return fullContent.substring(plistStart, plistEnd); 31 | } catch (Exception e) { 32 | LOGGER.log(Level.SEVERE, "Error extracting embedded XML", e); 33 | throw e; 34 | } 35 | } 36 | 37 | public static String parseProvisioningProfile(byte[] provisionData) { 38 | LOGGER.info("Starting to parse provisioning profile"); 39 | try { 40 | String xmlContent = extractEmbeddedXML(provisionData); 41 | byte[] contentBytes = xmlContent.getBytes(); 42 | 43 | // Use PlistUtils to handle binary or XML plist parsing 44 | if (PlistUtils.isBinaryPlist(contentBytes)) { 45 | LOGGER.info("Detected binary plist format, proceeding with binary parsing"); 46 | return PlistUtils.decodeBinaryPropertyList(contentBytes); 47 | } else { 48 | LOGGER.info("Detected XML plist format, proceeding with XML parsing"); 49 | NSObject parsedPlist = PropertyListParser.parse(contentBytes); 50 | return parsedPlist.toXMLPropertyList(); 51 | } 52 | } catch (Exception e) { 53 | LOGGER.log(Level.SEVERE, "Error parsing provisioning profile", e); 54 | return null; 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/security/KeyEncryption.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.security; 2 | 3 | import javax.crypto.Cipher; 4 | import javax.crypto.SecretKey; 5 | import javax.crypto.SecretKeyFactory; 6 | import javax.crypto.spec.PBEKeySpec; 7 | import javax.crypto.spec.SecretKeySpec; 8 | import java.nio.charset.StandardCharsets; 9 | import java.security.spec.KeySpec; 10 | import java.util.Base64; 11 | 12 | public class KeyEncryption { 13 | private static final String ALGORITHM = "AES"; 14 | private static final String SECRET = "MALIMITE_" + System.getProperty("user.name"); 15 | private static final byte[] SALT = "MALIMITE_SALT_123".getBytes(); 16 | 17 | private static SecretKey generateKey() throws Exception { 18 | SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); 19 | KeySpec spec = new PBEKeySpec(SECRET.toCharArray(), SALT, 65536, 256); 20 | return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), ALGORITHM); 21 | } 22 | 23 | public static String encrypt(String value) { 24 | try { 25 | if (value == null || value.isEmpty()) return ""; 26 | 27 | SecretKey key = generateKey(); 28 | Cipher cipher = Cipher.getInstance(ALGORITHM); 29 | cipher.init(Cipher.ENCRYPT_MODE, key); 30 | byte[] encryptedBytes = cipher.doFinal(value.getBytes()); 31 | return Base64.getEncoder().encodeToString(encryptedBytes); 32 | } catch (Exception e) { 33 | throw new RuntimeException("Error encrypting value", e); 34 | } 35 | } 36 | 37 | public static String decrypt(String encrypted) { 38 | try { 39 | if (encrypted == null || encrypted.isEmpty()) return ""; 40 | 41 | SecretKey key = generateKey(); 42 | Cipher cipher = Cipher.getInstance(ALGORITHM); 43 | cipher.init(Cipher.DECRYPT_MODE, key); 44 | byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encrypted)); 45 | return new String(decryptedBytes, StandardCharsets.UTF_8); 46 | } catch (Exception e) { 47 | throw new RuntimeException("Error decrypting value", e); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/tools/RuntimeMethodHandler.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.tools; 2 | 3 | import java.util.HashMap; 4 | import java.util.HashSet; 5 | import java.util.Map; 6 | import java.util.Set; 7 | 8 | public class RuntimeMethodHandler { 9 | private static final Map> methodCategories = new HashMap<>(); 10 | 11 | static { 12 | // Object Retain/Release 13 | addCategory("Object Retain/Release", 14 | "_swift_retain", 15 | "_swift_retain_n", 16 | "_swift_release", 17 | "_swift_release_n", 18 | "_swift_bridgeObjectRelease", 19 | "_swift_unknownObjectRelease", 20 | "_swift_unownedRetain", 21 | "_swift_unownedRelease", 22 | "_swift_retainCount", 23 | "_swift_bridgeObjectRetain", 24 | "_swift_unknownObjectRetain" 25 | ); 26 | 27 | // Dynamic Casting and Type Checking 28 | addCategory("Dynamic Casting", 29 | "_swift_dynamicCast", 30 | "_swift_dynamicCastClass", 31 | "_swift_dynamicCastClassUnconditional", 32 | "_swift_dynamicCastObjCClass", 33 | "_swift_dynamicCastObjCClassUnconditional", 34 | "_swift_dynamicCastMetatype", 35 | "_swift_dynamicCastMetatypeUnconditional", 36 | "_swift_isClassOrObjCExistentialType", 37 | "_swift_getGenericMetadata", 38 | "_swift_getGenericWitnessTable" 39 | ); 40 | 41 | // Memory Management 42 | addCategory("Memory Management", 43 | "_swift_allocObject", 44 | "_swift_deallocObject", 45 | "_swift_slowAlloc", 46 | "_swift_slowDealloc", 47 | "_swift_slowAllocArray", 48 | "_swift_bufferAllocate", 49 | "_swift_bufferDeallocate", 50 | "_swift_bufferHeaderInit", 51 | "_swift_bufferInitialize", 52 | "_swift_bufferDestroy" 53 | ); 54 | 55 | // String and Character 56 | addCategory("String Operations", 57 | "_swift_convertStringToNSString", 58 | "_swift_convertNSStringToString", 59 | "_swift_convertStringToCString", 60 | "_swift_convertCStringToString", 61 | "_swift_bridgeNSStringToString", 62 | "_swift_isStringEmpty", 63 | "_swift_strlen", 64 | "_swift_getStringLength" 65 | ); 66 | 67 | // Existentials 68 | addCategory("Existentials", 69 | "_swift_existentialRetain", 70 | "_swift_existentialRelease", 71 | "_swift_getExistentialTypeMetadata", 72 | "_swift_getExistentialMetatype", 73 | "_swift_getDynamicType" 74 | ); 75 | 76 | // Concurrency 77 | addCategory("Concurrency", 78 | "_swift_task_create", 79 | "_swift_task_suspend", 80 | "_swift_task_resume", 81 | "_swift_task_future_wait", 82 | "_swift_task_future_create", 83 | "_swift_task_detach", 84 | "_swift_task_group_create", 85 | "_swift_task_group_add", 86 | "_swift_task_group_wait", 87 | "_swift_task_isCancelled" 88 | ); 89 | 90 | // Bridging and Objective-C 91 | addCategory("Bridging", 92 | "_swift_bridgeObjectRetain", 93 | "_swift_bridgeObjectRelease", 94 | "_swift_bridgeFromObjectiveC", 95 | "_swift_bridgeToObjectiveC", 96 | "_swift_objc_getClass", 97 | "_swift_objc_getProtocol", 98 | "_swift_objc_getSelector", 99 | "_swift_objc_allocateClassPair", 100 | "_swift_objc_registerClassPair", 101 | "_swift_objc_autorelease" 102 | ); 103 | 104 | // Type Metadata 105 | addCategory("Type Metadata", 106 | "_swift_getClass", 107 | "_swift_getTypeByMangledName", 108 | "_swift_getTypeByMangledNameInContext", 109 | "_swift_getTypeMetadata", 110 | "_swift_getTypeMetadata2", 111 | "_swift_getTypeMetadata3", 112 | "_swift_getSuperclass", 113 | "_swift_getSuperclassTypeMetadata", 114 | "_swift_getWitnessTable", 115 | "_swift_getGenericWitnessTable", 116 | "_swift_getAssociatedTypeWitness" 117 | ); 118 | 119 | // Reflection 120 | addCategory("Reflection", 121 | "_swift_reflectionMirror_create", 122 | "_swift_reflectionMirror_destroy", 123 | "_swift_reflectionMetadataForClass", 124 | "_swift_reflectionMetadataForObject" 125 | ); 126 | 127 | // Error Handling 128 | addCategory("Error Handling", 129 | "_swift_getErrorType", 130 | "_swift_getErrorMetadata", 131 | "_swift_errorRetain", 132 | "_swift_errorRelease", 133 | "_swift_isError", 134 | "_swift_errorThrow", 135 | "_swift_errorCatch" 136 | ); 137 | 138 | // Protocol and Witness Tables 139 | addCategory("Protocol", 140 | "_swift_getProtocolConformance", 141 | "_swift_getWitnessTable", 142 | "_swift_getGenericWitnessTable", 143 | "_swift_getAssociatedTypeWitness", 144 | "_swift_protocolRequiresWitnessTable" 145 | ); 146 | 147 | // KeyPaths 148 | addCategory("KeyPaths", 149 | "_swift_keyPathRetain", 150 | "_swift_keyPathRelease", 151 | "_swift_keyPathAllocate", 152 | "_swift_keyPathCopy", 153 | "_swift_keyPathCreate" 154 | ); 155 | 156 | // Atomic Operations 157 | addCategory("Atomic", 158 | "_swift_atomicLoad", 159 | "_swift_atomicStore", 160 | "_swift_atomicCompareExchange", 161 | "_swift_atomicFetchAdd", 162 | "_swift_atomicFetchSub" 163 | ); 164 | 165 | // Miscellaneous 166 | addCategory("Miscellaneous", 167 | "_swift_once", 168 | "_swift_conformsToProtocol", 169 | "_swift_objectForKey", 170 | "_swift_setObjectForKey", 171 | "_swift_getAssociatedObject", 172 | "_swift_setAssociatedObject", 173 | "_swift_deallocateAssociatedObject", 174 | "_swift_getEnclosingContext", 175 | "_swift_currentContext", 176 | "_swift_getFieldAt" 177 | ); 178 | 179 | // Objective-C Memory Management 180 | addCategory("Objective-C Memory", 181 | "_objc_retain", 182 | "_objc_release", 183 | "_objc_retainAutoreleasedReturnValue", 184 | "_objc_retainAutorelease", 185 | "_objc_storeStrong", 186 | "_objc_loadWeak", 187 | "_objc_storeWeak", 188 | "_objc_copyWeak", 189 | "_objc_destroyWeak", 190 | "_objc_clearDeallocating", 191 | "_objc_autorelease", 192 | "_objc_autoreleasePoolPush", 193 | "_objc_autoreleasePoolPop", 194 | "_objc_autoreleaseReturnValue" 195 | ); 196 | 197 | // Objective-C Dynamic Messaging 198 | addCategory("Objective-C Messaging", 199 | "_objc_msgSend", 200 | "_objc_msgSendSuper", 201 | "_objc_msgSendSuper2", 202 | "_objc_msgSend_stret", 203 | "_objc_msgSend_fpret", 204 | "_objc_msgLookup", 205 | "_objc_msgForward", 206 | "_objc_msgForward_stret", 207 | "_objc_msgSendUncached" 208 | ); 209 | 210 | // Objective-C Class and Metaclass 211 | addCategory("Objective-C Class", 212 | "_objc_getClass", 213 | "_objc_getMetaClass", 214 | "_objc_getRequiredClass", 215 | "_objc_getClassList", 216 | "_objc_registerClassPair", 217 | "_objc_setFutureClass", 218 | "_objc_allocateClassPair", 219 | "_objc_disposeClassPair", 220 | "_objc_duplicateClass", 221 | "_objc_copyClassNamesForImage", 222 | "_objc_setHook_getClass", 223 | "_objc_setHook_getMetaClass" 224 | ); 225 | 226 | // Objective-C Property and KVC 227 | addCategory("Objective-C Property", 228 | "_objc_copyPropertyList", 229 | "_objc_getProperty", 230 | "_objc_setProperty", 231 | "_objc_copyPropertyAttributes", 232 | "_objc_setKey", 233 | "_objc_getKey", 234 | "_objc_getKeyPath", 235 | "_objc_setKeyPath" 236 | ); 237 | 238 | // Objective-C Protocol 239 | addCategory("Objective-C Protocol", 240 | "_objc_getProtocol", 241 | "_objc_registerProtocol", 242 | "_objc_conformsToProtocol", 243 | "_objc_allocateProtocol", 244 | "_objc_addProtocol", 245 | "_objc_setProtocolMethodTypes", 246 | "_objc_getSelector", 247 | "_objc_selector_register" 248 | ); 249 | 250 | // Objective-C Exception Handling 251 | addCategory("Objective-C Exceptions", 252 | "_objc_terminate", 253 | "_objc_begin_catch", 254 | "_objc_end_catch", 255 | "_objc_exception_throw", 256 | "_objc_setExceptionPreprocessor", 257 | "_objc_setExceptionHandler" 258 | ); 259 | 260 | // Objective-C Associated Objects 261 | addCategory("Objective-C Associated", 262 | "_objc_setAssociatedObject", 263 | "_objc_getAssociatedObject", 264 | "_objc_removeAssociatedObjects" 265 | ); 266 | 267 | // Objective-C Block Operations 268 | addCategory("Objective-C Blocks", 269 | "_objc_block_copy", 270 | "_objc_block_release", 271 | "_objc_block_store" 272 | ); 273 | 274 | // Objective-C Synchronization 275 | addCategory("Objective-C Sync", 276 | "_objc_sync_enter", 277 | "_objc_sync_exit", 278 | "_objc_initialize", 279 | "_objc_initializeClassPair", 280 | "_objc_fixupClassPair", 281 | "_objc_demangleClassName" 282 | ); 283 | } 284 | 285 | private static void addCategory(String category, String... methods) { 286 | Set methodSet = new HashSet<>(); 287 | for (String method : methods) { 288 | methodSet.add(method); 289 | } 290 | methodCategories.put(category, methodSet); 291 | } 292 | 293 | public static Set getMethodsInCategory(String category) { 294 | return methodCategories.getOrDefault(category, new HashSet<>()); 295 | } 296 | 297 | public static Set getAllMethods() { 298 | Set allMethods = new HashSet<>(); 299 | methodCategories.values().forEach(allMethods::addAll); 300 | return allMethods; 301 | } 302 | 303 | public static String getCategoryForMethod(String method) { 304 | for (Map.Entry> entry : methodCategories.entrySet()) { 305 | if (entry.getValue().contains(method)) { 306 | return entry.getKey(); 307 | } 308 | } 309 | return "Unknown"; 310 | } 311 | 312 | public static Set getCategories() { 313 | return methodCategories.keySet(); 314 | } 315 | 316 | public static boolean isSwiftRuntimeMethod(String method) { 317 | return getAllMethods().contains(method); 318 | } 319 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/ui/ApplicationMenu.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.ui; 2 | 3 | import javax.swing.*; 4 | import javax.swing.tree.DefaultMutableTreeNode; 5 | import javax.swing.tree.TreePath; 6 | 7 | import com.lauriewired.malimite.utils.NodeOperations; 8 | import com.lauriewired.malimite.configuration.Config; 9 | import java.awt.event.ActionListener; 10 | import java.awt.event.KeyEvent; 11 | 12 | import com.lauriewired.malimite.database.SQLiteDBHandler; 13 | import com.lauriewired.malimite.ui.AnalysisWindow; 14 | import com.lauriewired.malimite.ui.ReferenceHandler; 15 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; 16 | 17 | public class ApplicationMenu { 18 | private final JFrame parentFrame; 19 | private final JTree fileTree; 20 | private final Config config; 21 | 22 | public ApplicationMenu(JFrame parentFrame, JTree fileTree, Config config) { 23 | this.parentFrame = parentFrame; 24 | this.fileTree = fileTree; 25 | this.config = config; 26 | } 27 | 28 | public JMenuBar createMenuBar() { 29 | JMenuBar menuBar = new JMenuBar(); 30 | menuBar.add(createFileMenu()); 31 | menuBar.add(createViewMenu()); 32 | menuBar.add(createWindowsMenu()); 33 | menuBar.add(createHelpMenu()); 34 | return menuBar; 35 | } 36 | 37 | private JMenu createFileMenu() { 38 | JMenu fileMenu = new JMenu("File"); 39 | fileMenu.setMnemonic(KeyEvent.VK_F); 40 | 41 | addMenuItem(fileMenu, "Preferences...", e -> { 42 | SwingUtilities.invokeLater(() -> PreferencesDialog.show(parentFrame, config)); 43 | }, KeyStroke.getKeyStroke(KeyEvent.VK_COMMA, KeyEvent.CTRL_DOWN_MASK)); 44 | 45 | addMenuItem(fileMenu, "Close Window", e -> { 46 | parentFrame.dispose(); 47 | }, KeyStroke.getKeyStroke(KeyEvent.VK_W, KeyEvent.CTRL_DOWN_MASK)); 48 | 49 | fileMenu.addSeparator(); 50 | 51 | addMenuItem(fileMenu, "Configure Libraries", e -> { 52 | SwingUtilities.invokeLater(() -> LibraryConfigDialog.show(parentFrame, config)); 53 | }); 54 | 55 | addMenuItem(fileMenu, "Edit Function", e -> { 56 | TreePath path = fileTree.getSelectionPath(); 57 | if (path != null && path.getPathCount() == 4 && 58 | ((DefaultMutableTreeNode)path.getPathComponent(1)).getUserObject().toString().equals("Classes")) { 59 | AnalysisWindow.startEditing(path); 60 | } else { 61 | JOptionPane.showMessageDialog(parentFrame, 62 | "Please select a function in the Classes tree to edit.", 63 | "No Function Selected", 64 | JOptionPane.WARNING_MESSAGE); 65 | } 66 | }, KeyStroke.getKeyStroke(KeyEvent.VK_E, KeyEvent.CTRL_DOWN_MASK)); 67 | 68 | return fileMenu; 69 | } 70 | 71 | private JMenu createViewMenu() { 72 | JMenu viewMenu = new JMenu("View"); 73 | viewMenu.setMnemonic(KeyEvent.VK_V); 74 | 75 | if (fileTree != null) { 76 | addMenuItem(viewMenu, "Expand All", e -> 77 | NodeOperations.expandAllTreeNodes(fileTree), 78 | KeyStroke.getKeyStroke(KeyEvent.VK_E, KeyEvent.CTRL_DOWN_MASK) 79 | ); 80 | 81 | viewMenu.addSeparator(); 82 | 83 | addMenuItem(viewMenu, "Collapse All", e -> 84 | NodeOperations.collapseAllTreeNodes(fileTree), 85 | KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.CTRL_DOWN_MASK) 86 | ); 87 | 88 | viewMenu.addSeparator(); 89 | } 90 | 91 | JCheckBoxMenuItem lineWrapItem = new JCheckBoxMenuItem("Line Wrap"); 92 | lineWrapItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, KeyEvent.ALT_DOWN_MASK)); 93 | lineWrapItem.addActionListener(e -> { 94 | RSyntaxTextArea textArea = AnalysisWindow.getFileContentArea(); 95 | if (textArea != null) { 96 | textArea.setLineWrap(!textArea.getLineWrap()); 97 | textArea.setWrapStyleWord(textArea.getLineWrap()); 98 | } 99 | }); 100 | viewMenu.add(lineWrapItem); 101 | 102 | viewMenu.addSeparator(); 103 | 104 | addMenuItem(viewMenu, "Zoom In", e -> 105 | AnalysisWindow.zoomIn(), 106 | KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, KeyEvent.CTRL_DOWN_MASK) 107 | ); 108 | 109 | addMenuItem(viewMenu, "Zoom Out", e -> 110 | AnalysisWindow.zoomOut(), 111 | KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, KeyEvent.CTRL_DOWN_MASK) 112 | ); 113 | 114 | addMenuItem(viewMenu, "Reset Zoom", e -> 115 | AnalysisWindow.resetZoom(), 116 | KeyStroke.getKeyStroke(KeyEvent.VK_0, KeyEvent.CTRL_DOWN_MASK) 117 | ); 118 | 119 | return viewMenu; 120 | } 121 | 122 | private JMenu createWindowsMenu() { 123 | JMenu windowsMenu = new JMenu("Windows"); 124 | windowsMenu.setMnemonic(KeyEvent.VK_W); 125 | 126 | addMenuItem(windowsMenu, "Search", e -> { 127 | AnalysisWindow.toggleSearchPanel(); 128 | }, 129 | KeyStroke.getKeyStroke(KeyEvent.VK_F, 130 | config.isMac() ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK) 131 | ); 132 | 133 | addMenuItem(windowsMenu, "Search in Code", e -> { 134 | String searchTerm = JOptionPane.showInputDialog(parentFrame, 135 | "Enter search term (variable, method, or class name):", 136 | "Search in Code", 137 | JOptionPane.PLAIN_MESSAGE); 138 | 139 | if (searchTerm != null && !searchTerm.trim().isEmpty()) { 140 | SearchResultsDialog.show(parentFrame, AnalysisWindow.getDbHandler(), searchTerm.trim()); 141 | } 142 | }, 143 | KeyStroke.getKeyStroke(KeyEvent.VK_H, KeyEvent.CTRL_DOWN_MASK) 144 | ); 145 | 146 | addMenuItem(windowsMenu, "Right Panel", e -> { 147 | AnalysisWindow.toggleRightPanel(); 148 | }, 149 | KeyStroke.getKeyStroke(KeyEvent.VK_L, config.isMac() ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK) 150 | ); 151 | 152 | windowsMenu.addSeparator(); 153 | 154 | addMenuItem(windowsMenu, "Xrefs", e -> { 155 | String className = AnalysisWindow.getCurrentClassName(); 156 | String functionName = AnalysisWindow.getCurrentFunctionName(); 157 | String executableName = AnalysisWindow.getCurrentExecutableName(); 158 | SQLiteDBHandler dbHandler = AnalysisWindow.getDbHandler(); 159 | RSyntaxTextArea textArea = AnalysisWindow.getFileContentArea(); 160 | 161 | if (className != null && dbHandler != null && textArea != null) { 162 | ReferenceHandler.handleReferenceRequest(textArea, parentFrame, className, dbHandler, functionName, executableName); 163 | } 164 | }, 165 | KeyStroke.getKeyStroke(KeyEvent.VK_X, KeyEvent.CTRL_DOWN_MASK) 166 | ); 167 | 168 | windowsMenu.addSeparator(); 169 | 170 | addMenuItem(windowsMenu, "Entrypoints", e -> { 171 | EntrypointsDialog.show(parentFrame, AnalysisWindow.getDbHandler()); 172 | }); 173 | 174 | return windowsMenu; 175 | } 176 | 177 | private JMenu createHelpMenu() { 178 | JMenu helpMenu = new JMenu("Help"); 179 | helpMenu.setMnemonic(KeyEvent.VK_H); 180 | 181 | addMenuItem(helpMenu, "About", e -> 182 | JOptionPane.showMessageDialog(parentFrame, 183 | "Malimite - iOS and macOS Decompiler\nVersion 1.0\n© 2025", 184 | "About Malimite", 185 | JOptionPane.INFORMATION_MESSAGE) 186 | ); 187 | 188 | return helpMenu; 189 | } 190 | 191 | private void addMenuItem(JMenu menu, String text, ActionListener action, KeyStroke accelerator) { 192 | JMenuItem menuItem = new JMenuItem(text); 193 | if (accelerator != null) { 194 | menuItem.setAccelerator(accelerator); 195 | } 196 | menuItem.addActionListener(e -> SafeMenuAction.execute(() -> action.actionPerformed(e))); 197 | menu.add(menuItem); 198 | } 199 | 200 | private void addMenuItem(JMenu menu, String text, ActionListener action) { 201 | addMenuItem(menu, text, action, null); 202 | } 203 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/ui/CustomTokenMaker.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.ui; 2 | 3 | import javax.swing.text.Segment; 4 | import org.fife.ui.rsyntaxtextarea.*; 5 | import org.fife.ui.rsyntaxtextarea.modes.CPlusPlusTokenMaker; 6 | import com.lauriewired.malimite.tools.RuntimeMethodHandler; 7 | 8 | public class CustomTokenMaker extends CPlusPlusTokenMaker { 9 | // Custom token type for runtime methods 10 | public static final int RUNTIME_METHOD = Token.IDENTIFIER + 1; 11 | 12 | @Override 13 | public Token getTokenList(Segment text, int initialTokenType, int startOffset) { 14 | // Use parent method to generate initial token list 15 | Token tokenList = super.getTokenList(text, initialTokenType, startOffset); 16 | Token t = tokenList; 17 | 18 | // Traverse the token list 19 | while (t != null && t.isPaintable()) { 20 | if (t.getType() == TokenTypes.IDENTIFIER) { 21 | String lexeme = t.getLexeme(); 22 | 23 | // Check for runtime method prefixes 24 | if (lexeme.startsWith("_swift_") || lexeme.startsWith("_objc_")) { 25 | if (RuntimeMethodHandler.isSwiftRuntimeMethod(lexeme)) { 26 | t.setType(RUNTIME_METHOD); // Assign custom token type 27 | } 28 | } 29 | } 30 | t = t.getNextToken(); // Move to the next token 31 | } 32 | return tokenList; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/ui/EntrypointsDialog.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.ui; 2 | 3 | import javax.swing.*; 4 | import javax.swing.table.DefaultTableModel; 5 | import java.awt.*; 6 | import java.awt.event.MouseAdapter; 7 | import java.awt.event.MouseEvent; 8 | import java.util.*; 9 | import java.util.List; 10 | import com.lauriewired.malimite.database.SQLiteDBHandler; 11 | 12 | public class EntrypointsDialog { 13 | private static final Set ENTRYPOINT_FUNCTIONS = new HashSet<>(Arrays.asList( 14 | "applicationDidFinishLaunching", 15 | "application:didFinishLaunchingWithOptions", 16 | "applicationWillResignActive", 17 | "applicationDidEnterBackground", 18 | "applicationWillEnterForeground", 19 | "applicationDidBecomeActive", 20 | "applicationWillTerminate", 21 | "application:configurationForConnectingSceneSession:options", 22 | "application:didDiscardSceneSessions", 23 | "application:openURL:options", 24 | "application:performFetchWithCompletionHandler", 25 | "application:didReceiveRemoteNotification:fetchCompletionHandler", 26 | "application:handleEventsForBackgroundURLSession:completionHandler", 27 | "application:shouldSaveSecureApplicationState", 28 | "application:shouldRestoreSecureApplicationState", 29 | "application:didRegisterForRemoteNotificationsWithDeviceToken", 30 | "application:didFailToRegisterForRemoteNotificationsWithError", 31 | "application:didReceiveRemoteNotification", 32 | "application:handleOpenURL", 33 | "application:continueUserActivity:restorationHandler", 34 | "application:didUpdateUserActivity", 35 | "scene:willConnectToSession:options", 36 | "sceneDidDisconnect", 37 | "sceneDidBecomeActive", 38 | "sceneWillResignActive", 39 | "sceneWillEnterForeground", 40 | "sceneDidEnterBackground", 41 | "application:handleWatchKitExtensionRequest:reply", 42 | "main", 43 | "loadView", 44 | "viewDidLoad" 45 | )); 46 | 47 | public static void show(JFrame parentFrame, SQLiteDBHandler dbHandler) { 48 | JDialog dialog = new JDialog(parentFrame, "Entrypoints", true); 49 | dialog.setLayout(new BorderLayout()); 50 | 51 | // Create table model with column names 52 | DefaultTableModel tableModel = new DefaultTableModel( 53 | new String[]{"Class Name", "Entrypoint Function"}, 54 | 0 55 | ) { 56 | @Override 57 | public boolean isCellEditable(int row, int column) { 58 | return false; // Make table read-only 59 | } 60 | }; 61 | 62 | // Find all entrypoints 63 | Map> classesAndFunctions = dbHandler.getAllClassesAndFunctions(); 64 | List foundEntrypoints = new ArrayList<>(); 65 | 66 | for (Map.Entry> entry : classesAndFunctions.entrySet()) { 67 | String className = entry.getKey(); 68 | List functions = entry.getValue(); 69 | 70 | for (String function : functions) { 71 | if (ENTRYPOINT_FUNCTIONS.contains(function)) { 72 | foundEntrypoints.add(new String[]{className, function}); 73 | } 74 | } 75 | } 76 | 77 | // Sort entrypoints by class name, then function name 78 | Collections.sort(foundEntrypoints, (a, b) -> { 79 | int classCompare = a[0].compareTo(b[0]); 80 | return classCompare != 0 ? classCompare : a[1].compareTo(b[1]); 81 | }); 82 | 83 | // Add sorted entrypoints to table model 84 | for (String[] entrypoint : foundEntrypoints) { 85 | tableModel.addRow(entrypoint); 86 | } 87 | 88 | // Create and configure the JTable 89 | JTable entrypointTable = new JTable(tableModel); 90 | entrypointTable.setFillsViewportHeight(true); 91 | entrypointTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); 92 | 93 | // Make columns resize better 94 | entrypointTable.getColumnModel().getColumn(0).setPreferredWidth(200); 95 | entrypointTable.getColumnModel().getColumn(1).setPreferredWidth(300); 96 | 97 | // Add double-click listener 98 | entrypointTable.addMouseListener(new MouseAdapter() { 99 | @Override 100 | public void mouseClicked(MouseEvent e) { 101 | if (e.getClickCount() == 2) { 102 | int row = entrypointTable.rowAtPoint(e.getPoint()); 103 | if (row >= 0) { 104 | String className = (String) tableModel.getValueAt(row, 0); 105 | String functionName = (String) tableModel.getValueAt(row, 1); 106 | 107 | // Close the dialog 108 | dialog.dispose(); 109 | 110 | // Navigate to the selected function in the analysis window 111 | SwingUtilities.invokeLater(() -> { 112 | // Navigate to the function using the path format 113 | String path = "Classes/" + className + "/" + functionName; 114 | AnalysisWindow.showFileContent(path); 115 | }); 116 | } 117 | } 118 | } 119 | }); 120 | 121 | // Add the table to a scroll pane 122 | JScrollPane scrollPane = new JScrollPane(entrypointTable); 123 | dialog.add(scrollPane, BorderLayout.CENTER); 124 | 125 | // Add a close button at the bottom 126 | JButton closeButton = new JButton("Close"); 127 | closeButton.addActionListener(e -> dialog.dispose()); 128 | JPanel buttonPanel = new JPanel(); 129 | buttonPanel.add(closeButton); 130 | dialog.add(buttonPanel, BorderLayout.SOUTH); 131 | 132 | // Set dialog size and show it 133 | dialog.setSize(800, 500); 134 | dialog.setLocationRelativeTo(parentFrame); 135 | dialog.setVisible(true); 136 | } 137 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/ui/KeyboardShortcuts.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.ui; 2 | 3 | import javax.swing.KeyStroke; 4 | import javax.swing.SwingUtilities; 5 | import java.awt.event.KeyEvent; 6 | import java.awt.event.InputEvent; 7 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; 8 | import java.util.logging.Logger; 9 | import java.util.logging.Level; 10 | import javax.swing.JFrame; 11 | 12 | public class KeyboardShortcuts { 13 | private static final Logger LOGGER = Logger.getLogger(KeyboardShortcuts.class.getName()); 14 | 15 | public static void setupShortcuts(RSyntaxTextArea textArea, JFrame parentFrame) { 16 | // Add 'x' key binding for references 17 | textArea.getInputMap().put(KeyStroke.getKeyStroke('x'), "showReferences"); 18 | textArea.getActionMap().put("showReferences", new javax.swing.AbstractAction() { 19 | @Override 20 | public void actionPerformed(java.awt.event.ActionEvent e) { 21 | showReferencesForSelectedWord(textArea, parentFrame); 22 | } 23 | }); 24 | } 25 | 26 | private static void showReferencesForSelectedWord(RSyntaxTextArea textArea, JFrame parentFrame) { 27 | String selectedText = textArea.getSelectedText(); 28 | 29 | // If no text is selected, try to get the word at the cursor 30 | if (selectedText == null || selectedText.trim().isEmpty()) { 31 | try { 32 | int caretPos = textArea.getCaretPosition(); 33 | int start = getWordStart(textArea.getText(), caretPos); 34 | int end = getWordEnd(textArea.getText(), caretPos); 35 | if (start != -1 && end != -1) { 36 | selectedText = textArea.getText(start, end - start); 37 | } 38 | } catch (Exception ex) { 39 | LOGGER.log(Level.WARNING, "Error getting word at cursor", ex); 40 | return; 41 | } 42 | } 43 | 44 | // Only proceed if we have a word and we're in a class context 45 | if (selectedText != null && !selectedText.trim().isEmpty()) { 46 | String currentClassName = AnalysisWindow.getCurrentClassName(); 47 | if (currentClassName != null && !currentClassName.isEmpty()) { 48 | ReferencesDialog.show(parentFrame, 49 | AnalysisWindow.getDbHandler(), 50 | selectedText.trim(), 51 | AnalysisWindow.getCurrentClassName(), 52 | AnalysisWindow.getCurrentFunctionName(), 53 | AnalysisWindow.getCurrentExecutableName()); 54 | } 55 | } 56 | } 57 | 58 | private static int getWordStart(String text, int pos) { 59 | if (pos <= 0 || pos >= text.length()) return -1; 60 | 61 | while (pos > 0 && isWordChar(text.charAt(pos - 1))) { 62 | pos--; 63 | } 64 | return pos; 65 | } 66 | 67 | private static int getWordEnd(String text, int pos) { 68 | if (pos < 0 || pos >= text.length()) return -1; 69 | 70 | while (pos < text.length() && isWordChar(text.charAt(pos))) { 71 | pos++; 72 | } 73 | return pos; 74 | } 75 | 76 | private static boolean isWordChar(char c) { 77 | return Character.isLetterOrDigit(c) || c == '_'; 78 | } 79 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/ui/LibraryConfigDialog.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.ui; 2 | 3 | import com.lauriewired.malimite.configuration.LibraryDefinitions; 4 | import com.lauriewired.malimite.configuration.Config; 5 | import javax.swing.*; 6 | import java.awt.*; 7 | 8 | public class LibraryConfigDialog { 9 | public static void show(JFrame parent, Config config) { 10 | JDialog dialog = new JDialog(parent, "Configure Libraries", true); 11 | dialog.setLayout(new BorderLayout(10, 10)); 12 | dialog.setSize(450, 500); 13 | dialog.setLocationRelativeTo(parent); 14 | 15 | // Create info panel 16 | JPanel infoPanel = new JPanel(); 17 | infoPanel.setLayout(new BorderLayout()); 18 | infoPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); 19 | 20 | JTextArea infoText = new JTextArea( 21 | "Configure library names to optimize decompilation performance.\n\n" + 22 | "• Library names are case-sensitive\n" + 23 | "• Classes starting with these names will be treated as libraries\n" + 24 | "• Library classes will be skipped during decompilation\n" + 25 | "• Use this to ignore known frameworks and focus on app-specific code" 26 | ); 27 | infoText.setEditable(false); 28 | infoText.setBackground(null); 29 | infoText.setWrapStyleWord(true); 30 | infoText.setLineWrap(true); 31 | infoPanel.add(infoText, BorderLayout.CENTER); 32 | 33 | dialog.add(infoPanel, BorderLayout.NORTH); 34 | 35 | // Create list model and populate with active libraries 36 | DefaultListModel listModel = new DefaultListModel<>(); 37 | LibraryDefinitions.getActiveLibraries(config).forEach(listModel::addElement); 38 | 39 | // Create list with model 40 | JList libraryList = new JList<>(listModel); 41 | libraryList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 42 | 43 | // Add scroll pane 44 | JScrollPane scrollPane = new JScrollPane(libraryList); 45 | dialog.add(scrollPane, BorderLayout.CENTER); 46 | 47 | // Create button panel 48 | JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); 49 | 50 | // Add library button 51 | JButton addButton = new JButton("Add"); 52 | addButton.addActionListener(e -> { 53 | String input = JOptionPane.showInputDialog(dialog, 54 | "Enter library name:", 55 | "Add Library", 56 | JOptionPane.PLAIN_MESSAGE); 57 | 58 | if (input != null && !input.trim().isEmpty()) { 59 | String library = input.trim(); 60 | config.addLibrary(library); 61 | if (!listModel.contains(library)) { 62 | listModel.addElement(library); 63 | } 64 | } 65 | }); 66 | 67 | // Remove library button 68 | JButton removeButton = new JButton("Remove"); 69 | removeButton.addActionListener(e -> { 70 | int[] indices = libraryList.getSelectedIndices(); 71 | for (int i = indices.length - 1; i >= 0; i--) { 72 | String library = listModel.getElementAt(indices[i]); 73 | config.removeLibrary(library); 74 | listModel.remove(indices[i]); 75 | } 76 | }); 77 | 78 | // Restore defaults button 79 | JButton restoreButton = new JButton("Restore Defaults"); 80 | restoreButton.addActionListener(e -> { 81 | int result = JOptionPane.showConfirmDialog(dialog, 82 | "This will restore all libraries to their default settings.\nContinue?", 83 | "Restore Defaults", 84 | JOptionPane.YES_NO_OPTION, 85 | JOptionPane.WARNING_MESSAGE); 86 | 87 | if (result == JOptionPane.YES_OPTION) { 88 | config.clearLibraryConfigurations(); 89 | listModel.clear(); 90 | LibraryDefinitions.getDefaultLibraries().forEach(listModel::addElement); 91 | } 92 | }); 93 | 94 | // Close button 95 | JButton closeButton = new JButton("Close"); 96 | closeButton.addActionListener(e -> dialog.dispose()); 97 | 98 | buttonPanel.add(restoreButton); 99 | buttonPanel.add(addButton); 100 | buttonPanel.add(removeButton); 101 | buttonPanel.add(closeButton); 102 | dialog.add(buttonPanel, BorderLayout.SOUTH); 103 | 104 | dialog.setVisible(true); 105 | } 106 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/ui/PreferencesDialog.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.ui; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import com.lauriewired.malimite.configuration.Config; 6 | import com.lauriewired.malimite.utils.GhidraSetup; 7 | import com.lauriewired.malimite.Malimite; 8 | 9 | public class PreferencesDialog { 10 | private static JDialog dialog; 11 | 12 | public static void show(JFrame parent, Config config) { 13 | // Check if dialog is already showing 14 | if (dialog != null && dialog.isVisible()) { 15 | dialog.toFront(); 16 | return; 17 | } 18 | 19 | // Create the dialog 20 | dialog = new JDialog(parent, "Preferences", true); 21 | dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); 22 | 23 | // Create the main panel with some padding 24 | JPanel mainPanel = new JPanel(); 25 | mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); 26 | mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); 27 | 28 | // Theme selection 29 | JPanel themePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); 30 | themePanel.add(new JLabel("Theme:")); 31 | JComboBox themeComboBox = new JComboBox<>(new String[]{ 32 | "dark", 33 | "light" 34 | }); 35 | themeComboBox.setSelectedItem(config.getTheme()); 36 | themePanel.add(themeComboBox); 37 | mainPanel.add(themePanel); 38 | 39 | // Ghidra path setting 40 | JPanel ghidraPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); 41 | ghidraPanel.add(new JLabel("Ghidra Path:")); 42 | JTextField ghidraPathField = new JTextField(config.getGhidraPath(), 30); 43 | JButton browseButton = new JButton("Browse"); 44 | 45 | // Add help icon with tooltip 46 | JButton helpButton = new JButton("?"); 47 | helpButton.setPreferredSize(new Dimension(20, 20)); 48 | helpButton.setToolTipText("Path to root directory of Ghidra installation"); 49 | helpButton.setFocusPainted(false); 50 | 51 | browseButton.addActionListener(e -> { 52 | JFileChooser fileChooser = new JFileChooser(); 53 | fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); 54 | if (fileChooser.showOpenDialog(dialog) == JFileChooser.APPROVE_OPTION) { 55 | ghidraPathField.setText(fileChooser.getSelectedFile().getAbsolutePath()); 56 | } 57 | }); 58 | ghidraPanel.add(ghidraPathField); 59 | ghidraPanel.add(browseButton); 60 | ghidraPanel.add(helpButton); // Add the help button 61 | mainPanel.add(ghidraPanel); 62 | 63 | // Add some vertical spacing 64 | mainPanel.add(Box.createVerticalStrut(10)); 65 | 66 | // Add OpenAI API Key field 67 | JPanel openaiPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); 68 | openaiPanel.add(new JLabel("OpenAI API Key:")); 69 | JPasswordField openaiKeyField = new JPasswordField(config.getOpenAIApiKey(), 30); 70 | openaiPanel.add(openaiKeyField); 71 | mainPanel.add(openaiPanel); 72 | 73 | /* 74 | TODO Add back in when claude tested 75 | // Add Claude API Key field 76 | JPanel claudePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); 77 | claudePanel.add(new JLabel("Claude API Key:")); 78 | JPasswordField claudeKeyField = new JPasswordField(config.getClaudeApiKey(), 30); 79 | claudePanel.add(claudeKeyField); 80 | mainPanel.add(claudePanel); 81 | */ 82 | 83 | // Add Local Model URL field 84 | JPanel localModelPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); 85 | localModelPanel.add(new JLabel("Local Model URL:")); 86 | JTextField localModelUrlField = new JTextField(config.getLocalModelUrl(), 30); 87 | localModelPanel.add(localModelUrlField); 88 | mainPanel.add(localModelPanel); 89 | 90 | // Add some vertical spacing 91 | mainPanel.add(Box.createVerticalStrut(10)); 92 | 93 | // Buttons panel 94 | JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); 95 | JButton saveButton = new JButton("Save"); 96 | JButton cancelButton = new JButton("Cancel"); 97 | 98 | saveButton.addActionListener(e -> { 99 | String newTheme = (String) themeComboBox.getSelectedItem(); 100 | String currentTheme = config.getTheme(); 101 | String newGhidraPath = ghidraPathField.getText(); 102 | String currentGhidraPath = config.getGhidraPath(); 103 | 104 | // Check if Ghidra path has changed 105 | if (!newGhidraPath.equals(currentGhidraPath)) { 106 | int response = JOptionPane.showConfirmDialog(dialog, 107 | "Ghidra path updated. Downloading JSON dependency to Ghidra directory.\nNote: This will not affect Ghidra", 108 | "Setup Required Libraries", 109 | JOptionPane.OK_CANCEL_OPTION, 110 | JOptionPane.INFORMATION_MESSAGE); 111 | 112 | if (response == JOptionPane.OK_OPTION) { 113 | GhidraSetup.setupGhidraLibs(newGhidraPath); 114 | } 115 | } 116 | 117 | // Save all preferences 118 | config.setTheme(newTheme); 119 | config.setGhidraPath(newGhidraPath); 120 | config.setOpenAIApiKey(new String(openaiKeyField.getPassword())); 121 | //config.setClaudeApiKey(new String(claudeKeyField.getPassword())); 122 | config.setLocalModelUrl(localModelUrlField.getText()); 123 | config.saveConfig(); 124 | 125 | // Apply theme change if it was modified 126 | if (!newTheme.equals(currentTheme)) { 127 | Malimite.updateTheme(newTheme); 128 | } 129 | 130 | dialog.dispose(); 131 | }); 132 | 133 | cancelButton.addActionListener(e -> dialog.dispose()); 134 | 135 | buttonPanel.add(saveButton); 136 | buttonPanel.add(cancelButton); 137 | mainPanel.add(buttonPanel); 138 | 139 | // Add the main panel to the dialog 140 | dialog.add(mainPanel); 141 | 142 | // Size and position the dialog 143 | dialog.pack(); 144 | dialog.setLocationRelativeTo(parent); 145 | dialog.setResizable(false); 146 | 147 | // Show the dialog 148 | dialog.setVisible(true); 149 | } 150 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/ui/ReferenceHandler.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.ui; 2 | 3 | import javax.swing.*; 4 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; 5 | import java.awt.event.KeyEvent; 6 | import java.awt.event.KeyAdapter; 7 | import java.util.regex.Pattern; 8 | import java.util.regex.Matcher; 9 | 10 | import com.lauriewired.malimite.database.SQLiteDBHandler; 11 | 12 | public class ReferenceHandler { 13 | private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("[a-zA-Z_$][a-zA-Z0-9_$]*"); 14 | 15 | public static void attachTo(RSyntaxTextArea textArea, JFrame parent, String className, SQLiteDBHandler dbHandler, String functionName, String executableName) { 16 | textArea.addKeyListener(new KeyAdapter() { 17 | @Override 18 | public void keyPressed(KeyEvent e) { 19 | if (e.getKeyChar() == 'x' || e.getKeyChar() == 'X') { 20 | handleReferenceRequest(textArea, parent, className, dbHandler, functionName, executableName); 21 | } 22 | } 23 | }); 24 | } 25 | 26 | public static void handleReferenceRequest(RSyntaxTextArea textArea, JFrame parent, String className, SQLiteDBHandler dbHandler, String functionName, String executableName) { 27 | String selectedText = textArea.getSelectedText(); 28 | 29 | if (selectedText == null || selectedText.trim().isEmpty()) { 30 | // If no text is selected, try to get the word at cursor 31 | selectedText = getWordAtCursor(textArea); 32 | if (selectedText == null) return; 33 | } 34 | 35 | selectedText = selectedText.trim(); 36 | 37 | // Verify it's a valid identifier 38 | if (!IDENTIFIER_PATTERN.matcher(selectedText).matches()) { 39 | return; 40 | } 41 | 42 | // Show the references dialog 43 | ReferencesDialog.show(parent, dbHandler, selectedText, className, functionName, executableName); 44 | } 45 | 46 | public static void handleReferenceRequest(JFrame parent, String className, SQLiteDBHandler dbHandler, String functionName, String executableName) { 47 | // Show the references dialog without a specific selection 48 | ReferencesDialog.show(parent, dbHandler, null, className, functionName, executableName); 49 | } 50 | 51 | private static String getWordAtCursor(RSyntaxTextArea textArea) { 52 | try { 53 | int caretPos = textArea.getCaretPosition(); 54 | String text = textArea.getText(); 55 | 56 | // Find word boundaries 57 | int start = caretPos; 58 | int end = caretPos; 59 | 60 | // Look backwards for start of word 61 | while (start > 0 && isWordChar(text.charAt(start - 1))) { 62 | start--; 63 | } 64 | 65 | // Look forwards for end of word 66 | while (end < text.length() && isWordChar(text.charAt(end))) { 67 | end++; 68 | } 69 | 70 | if (start != end) { 71 | return text.substring(start, end); 72 | } 73 | } catch (Exception e) { 74 | // Handle any potential exceptions gracefully 75 | return null; 76 | } 77 | return null; 78 | } 79 | 80 | private static boolean isWordChar(char c) { 81 | return Character.isLetterOrDigit(c) || c == '_' || c == '$'; 82 | } 83 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/ui/ReferencesDialog.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.ui; 2 | 3 | import javax.swing.*; 4 | import javax.swing.table.DefaultTableModel; 5 | import java.awt.*; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | import com.lauriewired.malimite.database.SQLiteDBHandler; 10 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; 11 | import java.awt.Rectangle; 12 | 13 | public class ReferencesDialog { 14 | private static JDialog dialog; 15 | private static JTable referencesTable; 16 | private static DefaultTableModel tableModel; 17 | private static SQLiteDBHandler dbHandler; 18 | 19 | public static void show(JFrame parent, SQLiteDBHandler handler, String name, String className, String functionName, String executableName) { 20 | // Check if dialog is already showing 21 | if (dialog != null && dialog.isVisible()) { 22 | dialog.toFront(); 23 | return; 24 | } 25 | 26 | dbHandler = handler; 27 | 28 | // Determine if this is a local variable or function 29 | boolean isLocalVariable = isLocalVariable(name, className, functionName, executableName); 30 | 31 | // Create the dialog with appropriate title 32 | String title = isLocalVariable ? "Variable References" : "Function References"; 33 | dialog = new JDialog(parent, title, false); 34 | dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); 35 | 36 | // Create main panel with padding 37 | JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); 38 | mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); 39 | 40 | // Create table model with appropriate columns 41 | String[] columns; 42 | if (isLocalVariable) { 43 | columns = new String[]{"Type", "Variable", "Function", "Line"}; 44 | } else { 45 | columns = new String[]{"Type", "Source", "Target", "Line"}; 46 | } 47 | 48 | tableModel = new DefaultTableModel(columns, 0) { 49 | @Override 50 | public boolean isCellEditable(int row, int column) { 51 | return false; 52 | } 53 | }; 54 | 55 | // Create and configure the table 56 | referencesTable = new JTable(tableModel); 57 | referencesTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 58 | referencesTable.getTableHeader().setReorderingAllowed(false); 59 | 60 | // Add table to scroll pane 61 | JScrollPane scrollPane = new JScrollPane(referencesTable); 62 | scrollPane.setPreferredSize(new Dimension(600, 300)); 63 | mainPanel.add(scrollPane, BorderLayout.CENTER); 64 | 65 | // Create info panel at the top 66 | JPanel infoPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); 67 | String infoText = isLocalVariable ? 68 | String.format("References for variable '%s' in %s", name, className) : 69 | String.format("References for function '%s'", name); 70 | JLabel infoLabel = new JLabel(infoText); 71 | infoLabel.setFont(infoLabel.getFont().deriveFont(Font.BOLD)); 72 | infoPanel.add(infoLabel); 73 | 74 | // Add type information if it's a local variable 75 | if (isLocalVariable) { 76 | String type = getVariableType(name, className, executableName); 77 | if (type != null) { 78 | JLabel typeLabel = new JLabel(String.format(" (Type: %s)", type)); 79 | infoPanel.add(typeLabel); 80 | } 81 | } 82 | 83 | mainPanel.add(infoPanel, BorderLayout.NORTH); 84 | 85 | // Create button panel at the bottom 86 | JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); 87 | JButton closeButton = new JButton("Close"); 88 | closeButton.addActionListener(e -> dialog.dispose()); 89 | buttonPanel.add(closeButton); 90 | mainPanel.add(buttonPanel, BorderLayout.SOUTH); 91 | 92 | // Add the main panel to the dialog 93 | dialog.add(mainPanel); 94 | 95 | // Load references data 96 | if (isLocalVariable) { 97 | System.out.println("Loading local variable references for " + name + " in " + className); 98 | loadLocalVariableReferences(name, className, functionName, executableName); 99 | } else { 100 | System.out.println("Loading function references for " + name + " in " + className); 101 | loadFunctionReferences(name, className); 102 | } 103 | 104 | // Add mouse listener for double-click handling 105 | referencesTable.addMouseListener(new java.awt.event.MouseAdapter() { 106 | @Override 107 | public void mouseClicked(java.awt.event.MouseEvent evt) { 108 | if (evt.getClickCount() == 2) { 109 | int row = referencesTable.getSelectedRow(); 110 | if (row != -1) { 111 | handleDoubleClick(row, isLocalVariable); 112 | } 113 | } 114 | } 115 | }); 116 | 117 | // Size and position the dialog 118 | dialog.pack(); 119 | dialog.setLocationRelativeTo(parent); 120 | 121 | // Show the dialog 122 | dialog.setVisible(true); 123 | } 124 | 125 | private static boolean isLocalVariable(String name, String className, String functionName, String executableName) { 126 | // First check if it's a known function 127 | if (dbHandler.isFunctionName(name)) { 128 | return false; // It's a function 129 | } 130 | 131 | 132 | // Then check if it's a known variable in TypeInformation 133 | List> typeInfo = dbHandler.getTypeInformation(name, className, executableName); 134 | if (!typeInfo.isEmpty()) { 135 | return true; // It's a variable with type information 136 | } 137 | 138 | // Check for local variable references 139 | List> localVarRefs = dbHandler.getLocalVariableReferences(name, className, functionName, executableName); 140 | if (!localVarRefs.isEmpty()) { 141 | return true; // It's referenced as a local variable 142 | } 143 | 144 | // If we can't definitively determine it's a variable, assume it's a function 145 | return false; 146 | } 147 | 148 | private static String getVariableType(String variableName, String className, String executableName) { 149 | List> typeInfo = dbHandler.getTypeInformation(variableName, className, executableName); 150 | if (!typeInfo.isEmpty()) { 151 | return typeInfo.get(0).get("variableType"); 152 | } 153 | return null; 154 | } 155 | 156 | private static void loadLocalVariableReferences(String variableName, String className, String functionName, String executableName) { 157 | // Clear existing table data 158 | tableModel.setRowCount(0); 159 | 160 | // Get local variable references 161 | List> references = dbHandler.getLocalVariableReferences(variableName, className, functionName, executableName); 162 | 163 | // Add references to table 164 | for (Map reference : references) { 165 | String type = "LOCAL_VAR"; 166 | String variable = reference.get("variableName"); 167 | String function = formatReference(reference.get("containingFunction"), 168 | reference.get("containingClass")); 169 | String line = reference.get("lineNumber"); 170 | 171 | tableModel.addRow(new Object[]{type, variable, function, line}); 172 | } 173 | 174 | // Adjust column widths 175 | referencesTable.getColumnModel().getColumn(0).setPreferredWidth(100); // Type 176 | referencesTable.getColumnModel().getColumn(1).setPreferredWidth(150); // Variable 177 | referencesTable.getColumnModel().getColumn(2).setPreferredWidth(250); // Function 178 | referencesTable.getColumnModel().getColumn(3).setPreferredWidth(100); // Line 179 | } 180 | 181 | private static void loadFunctionReferences(String functionName, String className) { 182 | // Clear existing table data 183 | tableModel.setRowCount(0); 184 | 185 | // Get function references 186 | List> references = dbHandler.getFunctionCrossReferences(functionName); 187 | 188 | // Add references to table 189 | for (Map reference : references) { 190 | String type = reference.get("referenceType"); 191 | String source = formatReference(reference.get("sourceFunction"), 192 | reference.get("sourceClass")); 193 | String target = formatReference(reference.get("targetFunction"), 194 | reference.get("targetClass")); 195 | String line = reference.get("lineNumber"); 196 | 197 | tableModel.addRow(new Object[]{type, source, target, line}); 198 | } 199 | 200 | // Adjust column widths 201 | referencesTable.getColumnModel().getColumn(0).setPreferredWidth(100); // Type 202 | referencesTable.getColumnModel().getColumn(1).setPreferredWidth(200); // Source 203 | referencesTable.getColumnModel().getColumn(2).setPreferredWidth(200); // Target 204 | referencesTable.getColumnModel().getColumn(3).setPreferredWidth(100); // Line 205 | } 206 | 207 | private static String formatReference(String function, String className) { 208 | if (function == null || className == null) { 209 | return "Unknown"; 210 | } 211 | return String.format("%s::%s", className, function); 212 | } 213 | 214 | private static void handleDoubleClick(int row, boolean isLocalVariable) { 215 | String targetClass; 216 | String targetFunction; 217 | String lineNumber; 218 | 219 | if (isLocalVariable) { 220 | // For local variables, the function column contains "class::function" 221 | String functionRef = (String) tableModel.getValueAt(row, 2); 222 | String[] parts = functionRef.split("::"); 223 | if (parts.length == 2) { 224 | targetClass = parts[0]; 225 | targetFunction = parts[1]; 226 | } else { 227 | return; 228 | } 229 | lineNumber = (String) tableModel.getValueAt(row, 3); 230 | } else { 231 | // For function references, use the source column instead of target 232 | String sourceRef = (String) tableModel.getValueAt(row, 1); 233 | String[] parts = sourceRef.split("::"); 234 | if (parts.length == 2) { 235 | targetClass = parts[0]; 236 | targetFunction = parts[1]; 237 | } else { 238 | return; 239 | } 240 | lineNumber = (String) tableModel.getValueAt(row, 3); 241 | } 242 | 243 | // Navigate to the file and line 244 | SwingUtilities.invokeLater(() -> { 245 | // Build the path for the file (Classes/className/functionName) 246 | String filePath = String.format("Classes/%s/%s", targetClass, targetFunction); 247 | 248 | // Show the file content 249 | AnalysisWindow.showFileContent(filePath); 250 | 251 | // Navigate to line after content is loaded 252 | if (lineNumber != null) { 253 | try { 254 | int line = Integer.parseInt(lineNumber); 255 | AnalysisWindow.navigateToLine(line); 256 | } catch (NumberFormatException e) { 257 | System.err.println("Invalid line number: " + lineNumber); 258 | } 259 | } 260 | }); 261 | } 262 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/ui/SafeMenuAction.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.ui; 2 | 3 | import javax.swing.JDialog; 4 | import javax.swing.SwingUtilities; 5 | import java.util.logging.Logger; 6 | 7 | public class SafeMenuAction { 8 | private static final Logger LOGGER = Logger.getLogger(SafeMenuAction.class.getName()); 9 | private static boolean menuActionInProgress = false; 10 | private static final Object menuLock = new Object(); 11 | private static JDialog preferencesDialog = null; 12 | 13 | public static void execute(Runnable action) { 14 | synchronized (menuLock) { 15 | if (menuActionInProgress) { 16 | return; // Prevents simultaneous actions 17 | } 18 | menuActionInProgress = true; 19 | } 20 | 21 | SwingUtilities.invokeLater(() -> { 22 | try { 23 | action.run(); 24 | } catch (Exception ex) { 25 | LOGGER.severe("Error in menu action: " + ex.getMessage()); 26 | } finally { 27 | releaseMenuLock(); // Ensure the lock is always released 28 | } 29 | }); 30 | } 31 | 32 | private static void releaseMenuLock() { 33 | synchronized (menuLock) { 34 | menuActionInProgress = false; 35 | } 36 | } 37 | 38 | public static JDialog getPreferencesDialog() { 39 | return preferencesDialog; 40 | } 41 | 42 | public static void setPreferencesDialog(JDialog dialog) { 43 | cleanupPreferencesDialog(); // Ensure old dialog is disposed 44 | preferencesDialog = dialog; 45 | } 46 | 47 | public static void cleanupPreferencesDialog() { 48 | if (preferencesDialog != null) { 49 | preferencesDialog.dispose(); // Dispose of existing dialog to avoid conflicts 50 | preferencesDialog = null; 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/ui/SearchResultsDialog.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.ui; 2 | 3 | import javax.swing.*; 4 | import javax.swing.table.DefaultTableModel; 5 | import java.awt.*; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | import com.lauriewired.malimite.database.SQLiteDBHandler; 10 | 11 | public class SearchResultsDialog { 12 | private static JDialog dialog; 13 | private static JTable resultsTable; 14 | private static DefaultTableModel tableModel; 15 | 16 | public static void show(JFrame parent, SQLiteDBHandler dbHandler, String searchTerm) { 17 | // Check if dialog is already showing 18 | if (dialog != null && dialog.isVisible()) { 19 | dialog.dispose(); 20 | } 21 | 22 | // Create the dialog 23 | dialog = new JDialog(parent, "Search Results", false); 24 | dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); 25 | 26 | // Create main panel with padding 27 | JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); 28 | mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); 29 | 30 | // Create table model 31 | String[] columns = {"Type", "Name", "Location", "Line"}; 32 | tableModel = new DefaultTableModel(columns, 0) { 33 | @Override 34 | public boolean isCellEditable(int row, int column) { 35 | return false; 36 | } 37 | }; 38 | 39 | // Create and configure the table 40 | resultsTable = new JTable(tableModel); 41 | resultsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 42 | resultsTable.getTableHeader().setReorderingAllowed(false); 43 | 44 | // Add table to scroll pane 45 | JScrollPane scrollPane = new JScrollPane(resultsTable); 46 | scrollPane.setPreferredSize(new Dimension(800, 400)); 47 | mainPanel.add(scrollPane, BorderLayout.CENTER); 48 | 49 | // Create info panel at the top 50 | JPanel infoPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); 51 | JLabel infoLabel = new JLabel("Search results for: " + searchTerm); 52 | infoLabel.setFont(infoLabel.getFont().deriveFont(Font.BOLD)); 53 | infoPanel.add(infoLabel); 54 | mainPanel.add(infoPanel, BorderLayout.NORTH); 55 | 56 | // Create button panel at the bottom 57 | JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); 58 | JButton closeButton = new JButton("Close"); 59 | closeButton.addActionListener(e -> dialog.dispose()); 60 | buttonPanel.add(closeButton); 61 | mainPanel.add(buttonPanel, BorderLayout.SOUTH); 62 | 63 | // Add the main panel to the dialog 64 | dialog.add(mainPanel); 65 | 66 | // Load search results 67 | List> results = dbHandler.searchCodebase(searchTerm); 68 | loadSearchResults(results); 69 | 70 | // Add double-click handler 71 | resultsTable.addMouseListener(new java.awt.event.MouseAdapter() { 72 | @Override 73 | public void mouseClicked(java.awt.event.MouseEvent evt) { 74 | if (evt.getClickCount() == 2) { 75 | int row = resultsTable.getSelectedRow(); 76 | if (row != -1) { 77 | handleDoubleClick(row); 78 | } 79 | } 80 | } 81 | }); 82 | 83 | // Size and position the dialog 84 | dialog.pack(); 85 | dialog.setLocationRelativeTo(parent); 86 | 87 | // Show the dialog 88 | dialog.setVisible(true); 89 | } 90 | 91 | private static void loadSearchResults(List> results) { 92 | tableModel.setRowCount(0); 93 | 94 | for (Map result : results) { 95 | tableModel.addRow(new Object[]{ 96 | result.get("type"), 97 | result.get("name"), 98 | result.get("container"), 99 | result.get("line") 100 | }); 101 | } 102 | 103 | // Adjust column widths 104 | resultsTable.getColumnModel().getColumn(0).setPreferredWidth(100); // Type 105 | resultsTable.getColumnModel().getColumn(1).setPreferredWidth(200); // Name 106 | resultsTable.getColumnModel().getColumn(2).setPreferredWidth(400); // Location 107 | resultsTable.getColumnModel().getColumn(3).setPreferredWidth(100); // Line 108 | } 109 | 110 | private static void handleDoubleClick(int row) { 111 | String type = (String) tableModel.getValueAt(row, 0); 112 | String name = (String) tableModel.getValueAt(row, 1); 113 | String container = (String) tableModel.getValueAt(row, 2); 114 | String line = (String) tableModel.getValueAt(row, 3); 115 | 116 | SwingUtilities.invokeLater(() -> { 117 | String path; 118 | if ("Class".equals(type)) { 119 | path = "Classes/" + name; 120 | } else if ("Function".equals(type)) { 121 | path = "Classes/" + container + "/" + name; 122 | } else { // Variable 123 | String[] parts = container.split(" in "); 124 | if (parts.length == 2) { 125 | path = "Classes/" + parts[1] + "/" + parts[0]; 126 | } else { 127 | return; 128 | } 129 | } 130 | 131 | // Show the file content 132 | AnalysisWindow.showFileContent(path); 133 | 134 | // Navigate to line if available 135 | if (line != null && !line.isEmpty()) { 136 | try { 137 | int lineNum = Integer.parseInt(line); 138 | AnalysisWindow.navigateToLine(lineNum); 139 | } catch (NumberFormatException e) { 140 | System.err.println("Invalid line number: " + line); 141 | } 142 | } 143 | }); 144 | } 145 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/ui/SelectFile.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.ui; 2 | 3 | import javax.swing.BorderFactory; 4 | import javax.swing.JButton; 5 | import javax.swing.JLabel; 6 | import javax.swing.JPanel; 7 | import javax.swing.tree.DefaultMutableTreeNode; 8 | import javax.swing.tree.TreePath; 9 | 10 | import java.awt.BorderLayout; 11 | import java.awt.Color; 12 | import java.awt.Component; 13 | import java.awt.Cursor; 14 | import java.awt.FlowLayout; 15 | import java.awt.Insets; 16 | import java.awt.event.MouseAdapter; 17 | import java.awt.event.MouseEvent; 18 | import java.util.ArrayList; 19 | import java.util.HashMap; 20 | import java.util.List; 21 | import java.util.Map; 22 | import java.util.logging.Logger; 23 | import java.util.logging.Level; 24 | 25 | public class SelectFile { 26 | private static final Map openFiles = new HashMap<>(); 27 | private static final List fileOrder = new ArrayList<>(); 28 | private static final JPanel fileTabsContainer = new JPanel(new BorderLayout()); 29 | private static final JPanel fileTabsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 2, 0)); 30 | private static final JButton leftArrow = new JButton("◀"); 31 | private static final JButton rightArrow = new JButton("▶"); 32 | private static String activeFile = null; 33 | private static int scrollPosition = 0; 34 | private static final Logger LOGGER = Logger.getLogger(SelectFile.class.getName()); 35 | 36 | private static class FileTab extends JPanel { 37 | private boolean active = false; 38 | private final String filePath; 39 | private final JLabel nameLabel; 40 | private final JLabel closeButton; 41 | 42 | public FileTab(String path, Runnable onClose) { 43 | this.filePath = path; 44 | setLayout(new FlowLayout(FlowLayout.LEFT, 2, 0)); 45 | setOpaque(false); 46 | 47 | // Extract just the file name from the path 48 | String fileName = path.contains("/") ? 49 | path.substring(path.lastIndexOf('/') + 1) : path; 50 | 51 | nameLabel = new JLabel(fileName); 52 | nameLabel.setBorder(BorderFactory.createEmptyBorder(2, 3, 2, 3)); 53 | nameLabel.setCursor(new Cursor(Cursor.HAND_CURSOR)); 54 | 55 | closeButton = new JLabel("×"); 56 | closeButton.setFont(closeButton.getFont().deriveFont(14f)); 57 | closeButton.setCursor(new Cursor(Cursor.HAND_CURSOR)); 58 | closeButton.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); 59 | 60 | add(nameLabel); 61 | add(closeButton); 62 | 63 | this.addMouseListener(new MouseAdapter() { 64 | @Override 65 | public void mouseClicked(MouseEvent e) { 66 | if (e.getButton() == MouseEvent.BUTTON2) { 67 | onClose.run(); 68 | return; 69 | } 70 | setActiveFile(filePath); 71 | } 72 | 73 | @Override 74 | public void mouseEntered(MouseEvent e) { 75 | setHovered(true); 76 | } 77 | 78 | @Override 79 | public void mouseExited(MouseEvent e) { 80 | setHovered(false); 81 | } 82 | }); 83 | 84 | closeButton.addMouseListener(new MouseAdapter() { 85 | @Override 86 | public void mouseClicked(MouseEvent e) { 87 | onClose.run(); 88 | } 89 | }); 90 | } 91 | 92 | public void setActive(boolean active) { 93 | this.active = active; 94 | setHovered(active); 95 | if (active) { 96 | setBorder(BorderFactory.createCompoundBorder( 97 | BorderFactory.createMatteBorder(0, 0, 2, 0, Color.GRAY), 98 | BorderFactory.createEmptyBorder(2, 2, 0, 2) 99 | )); 100 | } else { 101 | setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); 102 | } 103 | } 104 | 105 | public void setHovered(boolean hovered) { 106 | nameLabel.setForeground(hovered || active ? Color.WHITE : new Color(187, 187, 187)); 107 | closeButton.setForeground(hovered || active ? Color.WHITE : new Color(187, 187, 187)); 108 | } 109 | } 110 | 111 | public static JPanel getFileTabsPanel() { 112 | // Setup scroll buttons 113 | leftArrow.setMargin(new Insets(0, 2, 0, 2)); 114 | rightArrow.setMargin(new Insets(0, 2, 0, 2)); 115 | 116 | leftArrow.addActionListener(e -> scroll(-1)); 117 | rightArrow.addActionListener(e -> scroll(1)); 118 | 119 | // Add components to container 120 | fileTabsContainer.add(leftArrow, BorderLayout.WEST); 121 | fileTabsContainer.add(fileTabsPanel, BorderLayout.CENTER); 122 | fileTabsContainer.add(rightArrow, BorderLayout.EAST); 123 | 124 | return fileTabsContainer; 125 | } 126 | 127 | public static void addFile(TreePath path) { 128 | //printDebugState("addFile - start"); 129 | DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); 130 | String filePath = buildPathFromNode(node); 131 | 132 | // Remove "Empty" tab if it exists 133 | Component[] components = fileTabsPanel.getComponents(); 134 | for (Component comp : components) { 135 | if (comp instanceof JLabel && "Empty".equals(((JLabel) comp).getText())) { 136 | fileTabsPanel.remove(comp); 137 | } 138 | } 139 | 140 | // Check if file is already open 141 | if (!openFiles.containsKey(filePath)) { 142 | FileTab tab = new FileTab(filePath, () -> closeFile(filePath)); 143 | openFiles.put(filePath, tab); 144 | fileOrder.add(filePath); 145 | 146 | // Add the tab to the panel 147 | fileTabsPanel.add(tab); 148 | } 149 | 150 | // Force layout update 151 | fileTabsPanel.revalidate(); 152 | fileTabsContainer.revalidate(); 153 | fileTabsPanel.repaint(); 154 | fileTabsContainer.repaint(); 155 | 156 | // Set as active file 157 | setActiveFile(filePath); 158 | //printDebugState("addFile - end"); 159 | } 160 | 161 | private static void closeFile(String filePath) { 162 | LOGGER.info("Closing file: " + filePath); 163 | FileTab tab = openFiles.remove(filePath); 164 | if (tab != null) { 165 | fileTabsPanel.remove(tab); 166 | fileOrder.remove(filePath); 167 | 168 | if (openFiles.isEmpty()) { 169 | addEmptyTab(); 170 | setActiveFile(null); 171 | } else if (filePath.equals(activeFile)) { 172 | int index = fileOrder.indexOf(filePath); 173 | if (index > 0) { 174 | setActiveFile(fileOrder.get(index - 1)); 175 | } else if (!fileOrder.isEmpty()) { 176 | setActiveFile(fileOrder.get(0)); 177 | } 178 | } 179 | 180 | fileTabsPanel.revalidate(); 181 | fileTabsPanel.repaint(); 182 | } 183 | } 184 | 185 | private static void setActiveFile(String filePath) { 186 | LOGGER.info("Setting active file: " + filePath); 187 | // Prevent unnecessary updates if the file is already active 188 | if (filePath != null && filePath.equals(activeFile)) { 189 | return; 190 | } 191 | 192 | activeFile = filePath; 193 | openFiles.forEach((path, tab) -> tab.setActive(path.equals(filePath))); 194 | 195 | // Force layout update 196 | fileTabsPanel.revalidate(); 197 | fileTabsContainer.revalidate(); 198 | fileTabsPanel.repaint(); 199 | fileTabsContainer.repaint(); 200 | 201 | // Always notify AnalysisWindow to update content, whether filePath is null or not 202 | AnalysisWindow.showFileContent(filePath); 203 | } 204 | 205 | private static String buildPathFromNode(DefaultMutableTreeNode node) { 206 | StringBuilder path = new StringBuilder(node.toString()); 207 | DefaultMutableTreeNode parent = (DefaultMutableTreeNode) node.getParent(); 208 | 209 | while (parent != null && !parent.isRoot()) { 210 | path.insert(0, parent.toString() + "/"); 211 | parent = (DefaultMutableTreeNode) parent.getParent(); 212 | } 213 | 214 | return path.toString(); 215 | } 216 | 217 | public static void clear() { 218 | //printDebugState("clear - start"); 219 | openFiles.clear(); 220 | fileOrder.clear(); 221 | fileTabsPanel.removeAll(); 222 | fileTabsPanel.revalidate(); 223 | fileTabsPanel.repaint(); 224 | activeFile = null; 225 | scrollPosition = 0; 226 | //printDebugState("clear - end"); 227 | } 228 | 229 | private static void addEmptyTab() { 230 | JLabel emptyLabel = new JLabel("Empty"); 231 | emptyLabel.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 5)); 232 | fileTabsPanel.add(emptyLabel); 233 | } 234 | 235 | private static void scroll(int direction) { 236 | //printDebugState("scroll - start"); 237 | if (fileOrder.isEmpty()) return; 238 | 239 | int currentIndex = activeFile != null ? fileOrder.indexOf(activeFile) : -1; 240 | int newIndex = currentIndex + direction; 241 | 242 | if (newIndex >= 0 && newIndex < fileOrder.size()) { 243 | String newFilePath = fileOrder.get(newIndex); 244 | setActiveFile(newFilePath); // Sets the new active file 245 | 246 | // Ensure this tab is visible in the viewport 247 | ensureTabVisible(newIndex); 248 | 249 | // Force content update in AnalysisWindow 250 | AnalysisWindow.showFileContent(newFilePath); 251 | } 252 | //printDebugState("scroll - end"); 253 | } 254 | 255 | private static void ensureTabVisible(int index) { 256 | if (index < 0 || index >= fileTabsPanel.getComponentCount()) return; 257 | 258 | Component tab = fileTabsPanel.getComponent(index); 259 | int tabLeft = tab.getX(); 260 | int tabRight = tabLeft + tab.getWidth(); 261 | int visibleWidth = fileTabsPanel.getParent().getWidth() - leftArrow.getWidth() - rightArrow.getWidth(); 262 | 263 | if (tabLeft < -scrollPosition) { 264 | // Scroll left to make the tab visible 265 | scrollPosition = -tabLeft; 266 | } else if (tabRight > -scrollPosition + visibleWidth) { 267 | // Scroll right to make the tab visible 268 | scrollPosition = -(tabRight - visibleWidth); 269 | } 270 | 271 | // Apply the scroll position to each tab component 272 | for (Component comp : fileTabsPanel.getComponents()) { 273 | comp.setLocation(comp.getX() - scrollPosition, comp.getY()); 274 | } 275 | 276 | fileTabsPanel.revalidate(); 277 | fileTabsPanel.repaint(); 278 | } 279 | 280 | public static void handleNodeClick(TreePath path) { 281 | DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); 282 | String filePath = buildPathFromNode(node); 283 | 284 | // Only update if the file is already open and not currently active 285 | if (openFiles.containsKey(filePath) && !filePath.equals(activeFile)) { 286 | setActiveFile(filePath); 287 | } 288 | } 289 | 290 | public static void replaceActiveFile(TreePath path) { 291 | LOGGER.info("Replacing active file: " + path); 292 | DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); 293 | String filePath = buildPathFromNode(node); 294 | 295 | // Remove current active file if it exists 296 | if (activeFile != null) { 297 | FileTab oldTab = openFiles.remove(activeFile); 298 | if (oldTab != null) { 299 | fileTabsPanel.remove(oldTab); 300 | fileOrder.remove(activeFile); 301 | } 302 | } 303 | 304 | // Add new file 305 | FileTab tab = new FileTab(filePath, () -> closeFile(filePath)); 306 | openFiles.put(filePath, tab); 307 | fileOrder.add(filePath); 308 | fileTabsPanel.add(tab); 309 | 310 | // Set as active 311 | setActiveFile(filePath); 312 | 313 | fileTabsPanel.revalidate(); 314 | fileTabsPanel.repaint(); 315 | } 316 | 317 | public static boolean isFileOpen(TreePath path) { 318 | DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); 319 | String filePath = buildPathFromNode(node); 320 | return openFiles.containsKey(filePath); 321 | } 322 | 323 | private static void printDebugState(String functionName) { 324 | LOGGER.fine(() -> "\n=== " + functionName + " ===\n" + 325 | "openFiles: " + openFiles.keySet() + "\n" + 326 | "fileOrder: " + fileOrder + "\n" + 327 | "activeFile: " + activeFile); 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/ui/SyntaxHighlighter.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.ui; 2 | 3 | import java.awt.Color; 4 | import java.awt.Container; 5 | import java.awt.Insets; 6 | 7 | import javax.swing.BorderFactory; 8 | import javax.swing.UIManager; 9 | 10 | import org.fife.ui.rtextarea.RTextScrollPane; 11 | import org.fife.ui.rtextarea.Gutter; 12 | import org.fife.ui.rsyntaxtextarea.AbstractTokenMakerFactory; 13 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; 14 | import org.fife.ui.rsyntaxtextarea.SyntaxConstants; 15 | import org.fife.ui.rsyntaxtextarea.SyntaxScheme; 16 | import org.fife.ui.rsyntaxtextarea.Token; 17 | import org.fife.ui.rsyntaxtextarea.TokenMakerFactory; 18 | import org.fife.ui.rtextarea.SmartHighlightPainter; 19 | import javax.swing.text.Highlighter; 20 | import java.awt.event.MouseAdapter; 21 | import java.awt.event.MouseEvent; 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | import java.util.logging.Level; 25 | import java.util.logging.Logger; 26 | import java.util.regex.Matcher; 27 | import java.util.regex.Pattern; 28 | import java.util.HashMap; 29 | import java.util.Map; 30 | import com.lauriewired.malimite.tools.RuntimeMethodHandler; 31 | import org.fife.ui.rsyntaxtextarea.Style; 32 | 33 | public class SyntaxHighlighter { 34 | 35 | private static final Logger LOGGER = Logger.getLogger(SyntaxHighlighter.class.getName()); 36 | private static final Color HIGHLIGHT_COLOR = new Color(255, 255, 0, 70); 37 | private static final List wordHighlights = new ArrayList<>(); // Track word highlights 38 | private static final Map customWordColors = new HashMap<>(); 39 | 40 | public static void applyCustomTheme(RSyntaxTextArea textArea) { 41 | // Register the custom TokenMaker for the C++ syntax style 42 | AbstractTokenMakerFactory factory = (AbstractTokenMakerFactory) TokenMakerFactory.getDefaultInstance(); 43 | factory.putMapping(SyntaxConstants.SYNTAX_STYLE_CPLUSPLUS, "com.lauriewired.malimite.ui.CustomTokenMaker"); 44 | 45 | textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_CPLUSPLUS); 46 | 47 | // Get the current theme's background color from UIManager 48 | Color themeBackground = UIManager.getColor("Panel.background"); 49 | if (themeBackground == null) { 50 | // Fallback if UIManager color is not available 51 | themeBackground = textArea.getBackground(); 52 | } 53 | 54 | boolean isDarkTheme = isDarkTheme(themeBackground); 55 | 56 | // Reset the syntax scheme to ensure clean state 57 | textArea.setSyntaxScheme(new SyntaxScheme(true)); 58 | SyntaxScheme scheme = textArea.getSyntaxScheme(); 59 | 60 | // Ensure the scheme can handle our custom token 61 | Color runtimeMethodColor = isDarkTheme 62 | ? new Color(210, 240, 255) // Very light blue for dark theme (much lighter than #9CDCFE) 63 | : new Color(0, 150, 255); // Brighter blue for light theme (lighter than #001080) 64 | scheme.setStyle(CustomTokenMaker.RUNTIME_METHOD, new Style(runtimeMethodColor)); 65 | 66 | // Background colors based on the theme 67 | Color editorBackground = isDarkTheme ? themeBackground : Color.WHITE; // Use white for light theme 68 | Color lineHighlight = isDarkTheme ? 69 | adjustBrightness(themeBackground, 1.2f) : 70 | new Color(240, 240, 240); // Light gray for light theme 71 | Color lineNumberBackground = isDarkTheme ? themeBackground : Color.WHITE; 72 | //Color lineNumberForeground = isDarkTheme ? Color.decode("#CCCCCC") : Color.decode("#333333"); 73 | 74 | // Set the syntax colors for the theme 75 | if (isDarkTheme) { 76 | Color dataTypeColor = Color.decode("#4EC9B0"); 77 | Color booleanColor = Color.decode("#569CD6"); 78 | scheme.getStyle(Token.RESERVED_WORD).foreground = Color.decode("#569CD6"); 79 | scheme.getStyle(Token.RESERVED_WORD_2).foreground = dataTypeColor; 80 | scheme.getStyle(Token.DATA_TYPE).foreground = dataTypeColor; 81 | scheme.getStyle(Token.FUNCTION).foreground = Color.decode("#DCDCAA"); 82 | scheme.getStyle(Token.LITERAL_NUMBER_DECIMAL_INT).foreground = Color.decode("#D7BA7D"); 83 | scheme.getStyle(Token.LITERAL_NUMBER_HEXADECIMAL).foreground = Color.decode("#D7BA7D"); 84 | scheme.getStyle(Token.LITERAL_BOOLEAN).foreground = booleanColor; 85 | scheme.getStyle(Token.LITERAL_STRING_DOUBLE_QUOTE).foreground = Color.decode("#CE9178"); 86 | scheme.getStyle(Token.COMMENT_MULTILINE).foreground = Color.decode("#57A64A"); 87 | scheme.getStyle(Token.COMMENT_DOCUMENTATION).foreground = Color.decode("#57A64A"); 88 | scheme.getStyle(Token.COMMENT_EOL).foreground = Color.decode("#57A64A"); 89 | scheme.getStyle(Token.OPERATOR).foreground = Color.WHITE; 90 | scheme.getStyle(Token.SEPARATOR).foreground = Color.WHITE; 91 | scheme.getStyle(Token.IDENTIFIER).foreground = Color.decode("#9CDCFE"); 92 | // XML-specific colors for dark theme 93 | scheme.getStyle(Token.MARKUP_TAG_DELIMITER).foreground = Color.decode("#808080"); // Gray for < > / 94 | scheme.getStyle(Token.MARKUP_TAG_NAME).foreground = Color.decode("#569CD6"); // Blue for tag names 95 | scheme.getStyle(Token.MARKUP_TAG_ATTRIBUTE).foreground = Color.decode("#9CDCFE"); // Light blue for attributes 96 | scheme.getStyle(Token.MARKUP_TAG_ATTRIBUTE_VALUE).foreground = Color.decode("#CE9178"); // Orange for values 97 | } else { 98 | Color booleanColor = Color.decode("#0451A5"); 99 | Color keyColor = Color.decode("#4A7A4F"); // Soft green for keys 100 | Color valueColor = Color.decode("#376E9B"); // Soft blue for values 101 | scheme.getStyle(Token.RESERVED_WORD).foreground = Color.decode("#0000FF"); 102 | scheme.getStyle(Token.DATA_TYPE).foreground = Color.decode("#267F99"); 103 | scheme.getStyle(Token.FUNCTION).foreground = Color.decode("#795E26"); 104 | scheme.getStyle(Token.LITERAL_NUMBER_DECIMAL_INT).foreground = Color.decode("#098658"); 105 | scheme.getStyle(Token.LITERAL_NUMBER_HEXADECIMAL).foreground = Color.decode("#098658"); 106 | scheme.getStyle(Token.LITERAL_BOOLEAN).foreground = booleanColor; 107 | scheme.getStyle(Token.LITERAL_STRING_DOUBLE_QUOTE).foreground = valueColor; // Soft blue for values 108 | scheme.getStyle(Token.MARKUP_TAG_NAME).foreground = keyColor; // Soft green for tag names (keys) 109 | scheme.getStyle(Token.COMMENT_MULTILINE).foreground = Color.decode("#008000"); 110 | scheme.getStyle(Token.COMMENT_DOCUMENTATION).foreground = Color.decode("#008000"); 111 | scheme.getStyle(Token.COMMENT_EOL).foreground = Color.decode("#008000"); 112 | scheme.getStyle(Token.OPERATOR).foreground = Color.decode("#333333"); 113 | scheme.getStyle(Token.SEPARATOR).foreground = Color.decode("#333333"); 114 | scheme.getStyle(Token.IDENTIFIER).foreground = Color.decode("#001080"); 115 | // XML-specific colors for light theme 116 | scheme.getStyle(Token.MARKUP_TAG_DELIMITER).foreground = Color.decode("#800000"); // Dark red for < > / 117 | scheme.getStyle(Token.MARKUP_TAG_NAME).foreground = Color.decode("#800000"); // Dark red for tag names 118 | scheme.getStyle(Token.MARKUP_TAG_ATTRIBUTE).foreground = Color.decode("#FF0000"); // Red for attributes 119 | scheme.getStyle(Token.MARKUP_TAG_ATTRIBUTE_VALUE).foreground = Color.decode("#0000FF"); // Blue for values 120 | } 121 | 122 | // Apply background colors 123 | textArea.setBackground(editorBackground); 124 | textArea.setCurrentLineHighlightColor(lineHighlight); 125 | textArea.setFadeCurrentLineHighlight(true); 126 | 127 | // Reset gutter colors directly to ensure the change applies 128 | Container parent = textArea.getParent(); 129 | if (parent != null && parent.getParent() instanceof RTextScrollPane) { 130 | RTextScrollPane scrollPane = (RTextScrollPane) parent.getParent(); 131 | Gutter gutter = scrollPane.getGutter(); 132 | gutter.setBackground(lineNumberBackground); 133 | //gutter.setLineNumberColor(lineNumberForeground); 134 | //gutter.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 0)); 135 | } 136 | 137 | // Add these lines after setting the background colors 138 | Color selectionColor = isDarkTheme ? 139 | new Color(51, 153, 255, 90) : // Semi-transparent blue for dark theme 140 | new Color(51, 153, 255, 50); // Lighter blue for light theme 141 | textArea.setSelectionColor(selectionColor); 142 | 143 | textArea.revalidate(); 144 | textArea.repaint(); 145 | } 146 | 147 | private static boolean isDarkTheme(Color background) { 148 | double brightness = (background.getRed() * 0.299 + 149 | background.getGreen() * 0.587 + 150 | background.getBlue() * 0.114) / 255; 151 | return brightness < 0.5; 152 | } 153 | 154 | private static Color adjustBrightness(Color color, float factor) { 155 | float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null); 156 | hsb[2] = Math.min(1.0f, hsb[2] * factor); // Adjust brightness 157 | return Color.getHSBColor(hsb[0], hsb[1], hsb[2]); 158 | } 159 | 160 | public static void setupWordHighlighting(RSyntaxTextArea textArea) { 161 | // Use a distinct color for word highlights 162 | Color wordHighlightColor = HIGHLIGHT_COLOR; 163 | Highlighter.HighlightPainter painter = new SmartHighlightPainter(wordHighlightColor); 164 | 165 | // Add a caret listener to dynamically highlight words at the caret 166 | textArea.addCaretListener(e -> { 167 | try { 168 | int caretPos = textArea.getCaretPosition(); 169 | highlightWordAtCaret(textArea, caretPos, painter); 170 | } catch (Exception ex) { 171 | LOGGER.log(Level.WARNING, "Error during word highlighting", ex); 172 | } 173 | }); 174 | } 175 | 176 | private static void highlightWordAtCaret(RSyntaxTextArea textArea, int caretPos, Highlighter.HighlightPainter painter) { 177 | try { 178 | // Remove only word highlights 179 | Highlighter highlighter = textArea.getHighlighter(); 180 | for (Object highlight : wordHighlights) { 181 | highlighter.removeHighlight(highlight); 182 | } 183 | wordHighlights.clear(); // Clear the tracking list 184 | 185 | // Get the word at the caret or the selected text 186 | String selectedText = textArea.getSelectedText(); 187 | if (selectedText == null || selectedText.trim().isEmpty()) { 188 | selectedText = getWordAtCaret(textArea.getText(), caretPos); 189 | } 190 | 191 | if (selectedText == null || selectedText.trim().isEmpty()) { 192 | return; 193 | } 194 | 195 | // Highlight all occurrences of the word 196 | String text = textArea.getText(); 197 | String wordRegex = "\\b" + Pattern.quote(selectedText.trim()) + "\\b"; 198 | Pattern pattern = Pattern.compile(wordRegex); 199 | Matcher matcher = pattern.matcher(text); 200 | 201 | while (matcher.find()) { 202 | Object highlight = highlighter.addHighlight(matcher.start(), matcher.end(), painter); 203 | wordHighlights.add(highlight); // Track this highlight 204 | } 205 | } catch (Exception ex) { 206 | LOGGER.log(Level.WARNING, "Error highlighting text", ex); 207 | } 208 | } 209 | 210 | private static String getWordAtCaret(String text, int caretPos) { 211 | if (caretPos < 0 || caretPos >= text.length()) return null; 212 | 213 | int start = caretPos; 214 | int end = caretPos; 215 | 216 | // Find the start of the word 217 | while (start > 0 && isWordChar(text.charAt(start - 1))) { 218 | start--; 219 | } 220 | 221 | // Find the end of the word 222 | while (end < text.length() && isWordChar(text.charAt(end))) { 223 | end++; 224 | } 225 | 226 | if (start < end) { 227 | return text.substring(start, end); 228 | } 229 | return null; 230 | } 231 | 232 | private static boolean isWordChar(char c) { 233 | return Character.isLetterOrDigit(c) || c == '_'; 234 | } 235 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/ui/WrapLayout.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.ui; 2 | 3 | import java.awt.*; 4 | 5 | public class WrapLayout extends FlowLayout { 6 | public WrapLayout(int align, int hgap, int vgap) { 7 | super(align, hgap, vgap); 8 | } 9 | 10 | @Override 11 | public Dimension preferredLayoutSize(Container target) { 12 | return layoutSize(target, true); 13 | } 14 | 15 | @Override 16 | public Dimension minimumLayoutSize(Container target) { 17 | return layoutSize(target, false); 18 | } 19 | 20 | private Dimension layoutSize(Container target, boolean preferred) { 21 | synchronized (target.getTreeLock()) { 22 | int width = target.getWidth(); 23 | if (width == 0) width = Integer.MAX_VALUE; 24 | 25 | Insets insets = target.getInsets(); 26 | int maxWidth = width - (insets.left + insets.right); 27 | int x = insets.left; 28 | int y = insets.top; 29 | int rowHeight = 0; 30 | 31 | int nmembers = target.getComponentCount(); 32 | for (int i = 0; i < nmembers; i++) { 33 | Component m = target.getComponent(i); 34 | if (m.isVisible()) { 35 | Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize(); 36 | if (x > insets.left && x + d.width > maxWidth) { 37 | x = insets.left; 38 | y += rowHeight + getVgap(); 39 | rowHeight = 0; 40 | } 41 | x += d.width + getHgap(); 42 | rowHeight = Math.max(rowHeight, d.height); 43 | } 44 | } 45 | return new Dimension(width, y + rowHeight + insets.bottom); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/utils/GhidraSetup.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.utils; 2 | 3 | import javax.swing.*; 4 | import java.io.*; 5 | import java.net.URL; 6 | import java.nio.file.*; 7 | 8 | public class GhidraSetup { 9 | private static final String JSON_VERSION = "20210307"; 10 | private static final String JSON_JAR = "json-" + JSON_VERSION + ".jar"; 11 | private static final String DOWNLOAD_URL = "https://repo1.maven.org/maven2/org/json/json/" + JSON_VERSION + "/" + JSON_JAR; 12 | 13 | private static boolean runAsAdmin(String[] command) { 14 | try { 15 | ProcessBuilder pb = new ProcessBuilder(command); 16 | pb.inheritIO(); 17 | Process process = pb.start(); 18 | return process.waitFor() == 0; 19 | } catch (Exception e) { 20 | e.printStackTrace(); 21 | return false; 22 | } 23 | } 24 | 25 | public static void setupGhidraLibs(String ghidraPath) { 26 | Path patchDir = Paths.get(ghidraPath, "Ghidra", "patch"); 27 | Path jsonJarPath = patchDir.resolve(JSON_JAR); 28 | 29 | // First try normal file operations 30 | try { 31 | Files.createDirectories(patchDir); 32 | URL url = new URL(DOWNLOAD_URL); 33 | try (InputStream in = url.openStream()) { 34 | Files.copy(in, jsonJarPath, StandardCopyOption.REPLACE_EXISTING); 35 | } 36 | JOptionPane.showMessageDialog(null, 37 | "Required library has been successfully installed.", 38 | "Setup Complete", 39 | JOptionPane.INFORMATION_MESSAGE); 40 | return; 41 | } catch (IOException e) { 42 | // If normal operation fails, try with admin privileges 43 | int response = JOptionPane.showConfirmDialog(null, 44 | "Failed to install library with normal permissions. Try with admin privileges?", 45 | "Permission Error", 46 | JOptionPane.YES_NO_OPTION, 47 | JOptionPane.QUESTION_MESSAGE); 48 | 49 | if (response == JOptionPane.YES_OPTION) { 50 | // Download to temp file first 51 | Path tempFile = null; 52 | try { 53 | tempFile = Files.createTempFile("ghidra_json_", ".jar"); 54 | URL url = new URL(DOWNLOAD_URL); 55 | try (InputStream in = url.openStream()) { 56 | Files.copy(in, tempFile, StandardCopyOption.REPLACE_EXISTING); 57 | } 58 | 59 | // Use sudo commands to create directory and copy file 60 | String[] mkdirCommand = {"sudo", "mkdir", "-p", patchDir.toString()}; 61 | String[] copyCommand = {"sudo", "cp", tempFile.toString(), jsonJarPath.toString()}; 62 | 63 | if (runAsAdmin(mkdirCommand) && runAsAdmin(copyCommand)) { 64 | JOptionPane.showMessageDialog(null, 65 | "Required library has been successfully installed with admin privileges.", 66 | "Setup Complete", 67 | JOptionPane.INFORMATION_MESSAGE); 68 | return; 69 | } 70 | } catch (IOException ex) { 71 | ex.printStackTrace(); 72 | } finally { 73 | if (tempFile != null) { 74 | try { 75 | Files.deleteIfExists(tempFile); 76 | } catch (IOException ignored) {} 77 | } 78 | } 79 | } 80 | 81 | // If everything fails, show error message 82 | JOptionPane.showMessageDialog(null, 83 | "Failed to install library: " + e.getMessage(), 84 | "Setup Error", 85 | JOptionPane.ERROR_MESSAGE); 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/utils/NodeOperations.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.utils; 2 | 3 | import java.util.Enumeration; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.logging.Logger; 7 | 8 | import javax.swing.JTree; 9 | import javax.swing.tree.DefaultMutableTreeNode; 10 | import javax.swing.tree.TreeNode; 11 | 12 | public class NodeOperations { 13 | private static final Logger LOGGER = Logger.getLogger(NodeOperations.class.getName()); 14 | 15 | public static String buildFullPathFromNode(TreeNode node) { 16 | StringBuilder fullPath = new StringBuilder(); 17 | String nodeString = ""; 18 | 19 | while (node != null) { 20 | nodeString = node.toString(); 21 | 22 | // Avoid adding the prepended "Files" node 23 | if (node.getParent() != null && nodeString != "Hidden") { 24 | //System.out.println("fullPath: " + fullPath.toString()); 25 | //System.out.println("node: " + nodeString); 26 | 27 | // Insert slash into path only if needed 28 | if (fullPath.length() > 0 && fullPath.charAt(0) != '/' && nodeString.charAt(nodeString.length() - 1) != '/') { 29 | fullPath.insert(0, "/"); 30 | } 31 | fullPath.insert(0, nodeString); 32 | } 33 | 34 | node = node.getParent(); 35 | } 36 | return fullPath.toString(); 37 | } 38 | 39 | public static void collapseAllTreeNodes(JTree fileTree) { 40 | for (int i = 0; i < fileTree.getRowCount(); i++) { 41 | fileTree.collapseRow(i); 42 | } 43 | } 44 | 45 | public static DefaultMutableTreeNode addOrGetNode(DefaultMutableTreeNode parentNode, String nodeName, boolean isDirectory) { 46 | Enumeration children = parentNode.children(); 47 | while (children.hasMoreElements()) { 48 | DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) children.nextElement(); 49 | if (childNode.getUserObject().equals(nodeName) && childNode.getAllowsChildren() == isDirectory) { 50 | return childNode; 51 | } 52 | } 53 | 54 | DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(nodeName, isDirectory); 55 | parentNode.add(newNode); 56 | return newNode; 57 | } 58 | 59 | public static void expandAllTreeNodes(JTree fileTree) { 60 | for (int i = 0; i < fileTree.getRowCount(); i++) { 61 | fileTree.expandRow(i); 62 | } 63 | } 64 | 65 | public static DefaultMutableTreeNode findInfoPlistNode(DefaultMutableTreeNode root) { 66 | // Find the Files node first 67 | for (int i = 0; i < root.getChildCount(); i++) { 68 | DefaultMutableTreeNode filesNode = (DefaultMutableTreeNode) root.getChildAt(i); 69 | 70 | if (filesNode.getUserObject().toString().equals("Files")) { 71 | // Look for the .app directory directly under Files 72 | for (int j = 0; j < filesNode.getChildCount(); j++) { 73 | DefaultMutableTreeNode appNode = (DefaultMutableTreeNode) filesNode.getChildAt(j); 74 | if (appNode.getUserObject().toString().endsWith(".app/")) { 75 | // Look for Info.plist directly under the .app directory 76 | for (int k = 0; k < appNode.getChildCount(); k++) { 77 | DefaultMutableTreeNode child = (DefaultMutableTreeNode) appNode.getChildAt(k); 78 | if (child.getUserObject().toString().equals("Info.plist")) { 79 | return child; 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | return null; 87 | } 88 | 89 | public static void populateClassesNode(DefaultMutableTreeNode classesRootNode, 90 | Map> classesAndFunctions) { 91 | LOGGER.info("Populating classes tree..."); 92 | classesRootNode.removeAllChildren(); 93 | LOGGER.info("Retrieved " + classesAndFunctions.size() + " classes from database"); 94 | 95 | for (Map.Entry> entry : classesAndFunctions.entrySet()) { 96 | String className = entry.getKey(); 97 | List functions = entry.getValue(); 98 | LOGGER.fine("Adding class: " + className + " with " + functions.size() + " functions"); 99 | DefaultMutableTreeNode classNode = new DefaultMutableTreeNode(className); 100 | for (String function : functions) { 101 | classNode.add(new DefaultMutableTreeNode(function)); 102 | } 103 | classesRootNode.add(classNode); 104 | } 105 | LOGGER.info("Finished populating classes tree"); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/utils/PlistUtils.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.utils; 2 | 3 | import java.util.Arrays; 4 | 5 | import com.dd.plist.NSObject; 6 | import com.dd.plist.PropertyListParser; 7 | import com.google.gson.Gson; 8 | import com.google.gson.GsonBuilder; 9 | 10 | public class PlistUtils { 11 | /* 12 | * Returns true if it's a binary plist 13 | * Returns false if it is an XML plist 14 | */ 15 | public static boolean isBinaryPlist(byte[] contentBytes) { 16 | if (contentBytes.length < "bplist".length()) { 17 | return false; 18 | } 19 | String header = new String(Arrays.copyOf(contentBytes, "bplist".length())); 20 | 21 | return header.equals("bplist"); 22 | } 23 | 24 | /* 25 | * Inputs the binary plist as a byte array 26 | * Returns the decoded plist in JSON format 27 | * Do it this way because using the plist library instead of built-in mac libs makes this cross-platform 28 | */ 29 | public static String decodeBinaryPropertyList(byte[] plistData) { 30 | try { 31 | NSObject plist = PropertyListParser.parse(plistData); 32 | Object javaObj = plist.toJavaObject(); 33 | 34 | // Use Gson to format it as a JSON string 35 | Gson gson = new GsonBuilder().setPrettyPrinting().create(); 36 | return gson.toJson(javaObj); 37 | } catch (Exception e) { 38 | e.printStackTrace(); 39 | return null; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/malimite/utils/ResourceParser.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired.malimite.utils; 2 | 3 | import java.io.*; 4 | import java.nio.charset.StandardCharsets; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | import java.util.regex.Pattern; 8 | import java.util.logging.Logger; 9 | import com.lauriewired.malimite.files.MobileProvision; 10 | import com.lauriewired.malimite.database.SQLiteDBHandler; 11 | public class ResourceParser { 12 | 13 | private static SQLiteDBHandler dbHandler; 14 | private static final Logger LOGGER = Logger.getLogger(ResourceParser.class.getName()); 15 | 16 | public static void setDatabaseHandler(SQLiteDBHandler handler) { 17 | dbHandler = handler; 18 | } 19 | 20 | // Predefined patterns for identifying resource files 21 | private static final List RESOURCE_PATTERNS = Arrays.asList( 22 | Pattern.compile(".*\\.plist$"), // Property list files 23 | Pattern.compile(".*\\.strings$"), // Localization string files 24 | Pattern.compile(".*\\.json$"), // JSON configuration files 25 | Pattern.compile(".*\\.xml$"), // XML files 26 | Pattern.compile(".*\\.mobileprovision$"), // Provisioning profiles 27 | Pattern.compile(".*\\.storyboardc$"), // Interface builder files 28 | Pattern.compile(".*\\.xcassets$"), // Asset catalogs 29 | Pattern.compile(".*\\.nib$"), // Interface builder files 30 | Pattern.compile(".*\\.xib$") // Interface builder files (newly added) 31 | ); 32 | 33 | /** 34 | * Checks if a file name matches any predefined resource pattern. 35 | */ 36 | public static boolean isResource(String fileName) { 37 | LOGGER.fine("Checking if file is a resource: " + fileName); 38 | for (Pattern pattern : RESOURCE_PATTERNS) { 39 | if (pattern.matcher(fileName).matches()) { 40 | LOGGER.fine("File identified as resource: " + fileName); 41 | return true; 42 | } 43 | } 44 | return false; 45 | } 46 | 47 | /** 48 | * Parses a resource file for readable strings from an input stream. 49 | * This function handles text-based resources and excludes binary data. 50 | */ 51 | public static void parseResourceForStrings(InputStream inputStream, String fileName) { 52 | try { 53 | // Convert input stream to byte array for multiple reads 54 | ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 55 | int nRead; 56 | byte[] data = new byte[4096]; 57 | while ((nRead = inputStream.read(data, 0, data.length)) != -1) { 58 | buffer.write(data, 0, nRead); 59 | } 60 | byte[] contentBytes = buffer.toByteArray(); 61 | 62 | LOGGER.info("Processing file: " + fileName); 63 | 64 | // Handle different file types 65 | String content; 66 | if (fileName.endsWith(".plist")) { 67 | content = handlePlist(contentBytes); 68 | LOGGER.fine("Processed as plist file"); 69 | } else if (fileName.endsWith("embedded.mobileprovision")) { 70 | content = MobileProvision.extractEmbeddedXML(contentBytes); 71 | LOGGER.fine("Processed as mobileprovision file"); 72 | } else { 73 | content = new String(contentBytes, StandardCharsets.UTF_8); 74 | LOGGER.fine("Processed as regular text file"); 75 | } 76 | 77 | // Process the content line by line 78 | try (BufferedReader reader = new BufferedReader(new StringReader(content))) { 79 | String line; 80 | int lineCount = 0; 81 | while ((line = reader.readLine()) != null) { 82 | lineCount++; 83 | 84 | if (!line.trim().isEmpty()) { 85 | String[] segments = line.split("[^\\p{Print}]+"); 86 | for (String segment : segments) { 87 | String trimmedSegment = segment.trim(); 88 | if (!trimmedSegment.isEmpty() && trimmedSegment.replaceAll("\\s+", "").length() > 4) { 89 | if (dbHandler != null) { 90 | // Store the trimmed segment 91 | dbHandler.insertResourceString(fileName, trimmedSegment, getResourceType(fileName)); 92 | LOGGER.fine("Inserted resource string: " + trimmedSegment + " for path: " + fileName); 93 | } 94 | } 95 | } 96 | } 97 | } 98 | LOGGER.info("Completed processing " + lineCount + " lines"); 99 | } 100 | } catch (IOException e) { 101 | LOGGER.severe("Error reading file: " + e.getMessage()); 102 | } catch (Exception e) { 103 | LOGGER.severe("Error processing file: " + e.getMessage()); 104 | } 105 | } 106 | 107 | private static String handlePlist(byte[] contentBytes) throws Exception { 108 | if (PlistUtils.isBinaryPlist(contentBytes)) { 109 | return PlistUtils.decodeBinaryPropertyList(contentBytes); 110 | } 111 | return new String(contentBytes, StandardCharsets.UTF_8); 112 | } 113 | 114 | private static String getResourceType(String fileName) { 115 | if (fileName.endsWith(".plist")) return "plist"; 116 | if (fileName.endsWith(".strings")) return "strings"; 117 | if (fileName.endsWith(".json")) return "json"; 118 | if (fileName.endsWith(".xml")) return "xml"; 119 | if (fileName.endsWith(".mobileprovision")) return "mobileprovision"; 120 | if (fileName.endsWith(".storyboardc")) return "storyboard"; 121 | if (fileName.endsWith(".xcassets")) return "assets"; 122 | if (fileName.endsWith(".nib")) return "nib"; 123 | return "unknown"; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/resources/icons/app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurieWired/Malimite/89d274f699896beaa3d38325c6b8952fc7a9b25d/src/main/resources/icons/app-icon.png --------------------------------------------------------------------------------