-
Notifications
You must be signed in to change notification settings - Fork 6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
GP-4795: Initial support for Visual Studio Code script and module
development
- Loading branch information
1 parent
fd1fb15
commit 73018ad
Showing
3 changed files
with
296 additions
and
11 deletions.
There are no files selected for viewing
277 changes: 277 additions & 0 deletions
277
Ghidra/Features/Base/ghidra_scripts/VSCodeProjectScript.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,277 @@ | ||
/* ### | ||
* IP: GHIDRA | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
// Creates a new VSCode project for Ghidra script and module development. | ||
// @category Development | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.*; | ||
|
||
import org.apache.commons.io.*; | ||
|
||
import com.google.gson.*; | ||
|
||
import ghidra.*; | ||
import ghidra.app.script.GhidraScript; | ||
import ghidra.features.base.values.GhidraValuesMap; | ||
import ghidra.framework.Application; | ||
import ghidra.framework.ApplicationProperties; | ||
import ghidra.util.SystemUtilities; | ||
import utilities.util.FileUtilities; | ||
|
||
public class VSCodeProjectScript extends GhidraScript { | ||
|
||
private Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); | ||
|
||
@Override | ||
protected void run() throws Exception { | ||
if (!SystemUtilities.isInReleaseMode()) { | ||
printerr("This script may only run from a built Ghidra release."); | ||
return; | ||
} | ||
|
||
final String PROJECT_NAME_PROMPT = "Project name"; | ||
final String PROJECT_ROOT_PROMPT = "Project root directory"; | ||
|
||
GhidraValuesMap values = new GhidraValuesMap(); | ||
values.defineString(PROJECT_NAME_PROMPT); | ||
values.defineDirectory(PROJECT_ROOT_PROMPT, new File(System.getProperty("user.home"))); | ||
values = askValues("Setup New VSCode Project", null, values); | ||
|
||
String projectName = values.getString(PROJECT_NAME_PROMPT); | ||
File projectRootDir = values.getFile(PROJECT_ROOT_PROMPT); | ||
File projectDir = new File(projectRootDir, projectName); | ||
if (projectDir.exists()) { | ||
printerr("Directory '%s' already exists...exiting".formatted(projectDir)); | ||
return; | ||
} | ||
|
||
File installDir = Application.getInstallationDirectory().getFile(false); | ||
Map<String, String> classpathSourceMap = getClasspathSourceMap(); | ||
writeSettings(installDir, projectDir, classpathSourceMap); | ||
writeLaunch(installDir, projectDir, classpathSourceMap); | ||
writeSampleScriptJava(projectDir); | ||
writeSampleModule(installDir, projectDir); | ||
|
||
println("Successfully created VSCode project directory at: " + projectDir); | ||
println( | ||
"To debug, please close Ghidra and relaunch from the VSCode Ghidra launch configuration."); | ||
} | ||
|
||
/** | ||
* Gets the classpath and source of the currently running Ghidra | ||
* | ||
* @return A {@link Map} of classpath jars to their corresponding source zip files (source zip | ||
* could be null if it doesn't exist) | ||
* @throws IOException if an IO-related error occurs | ||
*/ | ||
private Map<String, String> getClasspathSourceMap() throws IOException { | ||
Map<String, String> classpathSourceMap = new LinkedHashMap<>(); | ||
for (String entry : GhidraClassLoader.getClasspath(GhidraClassLoader.CP)) { | ||
entry = new File(entry).getCanonicalPath(); | ||
String sourcePath = entry.substring(0, entry.length() - 4) + "-src.zip"; | ||
if (!entry.endsWith(".jar") || !new File(sourcePath).exists()) { | ||
sourcePath = null; | ||
} | ||
classpathSourceMap.put(entry, sourcePath); | ||
} | ||
return classpathSourceMap; | ||
} | ||
|
||
/** | ||
* Write the .vscode/settings.json file | ||
* | ||
* @param installDir The Ghidra installation directory | ||
* @param projectDir The VSCode project directory | ||
* @param classpathSourceMap The classpath/source map (see {@link #getClasspathSourceMap()}) | ||
* @throws IOException if an IO-related error occurs | ||
*/ | ||
private void writeSettings(File installDir, File projectDir, | ||
Map<String, String> classpathSourceMap) throws IOException { | ||
File vscodeDir = new File(projectDir, ".vscode"); | ||
File settingsFile = new File(vscodeDir, "settings.json"); | ||
String gradleVersion = Application | ||
.getApplicationProperty(ApplicationProperties.APPLICATION_GRADLE_MIN_PROPERTY); | ||
|
||
// Build settings json object | ||
JsonObject json = new JsonObject(); | ||
json.addProperty("java.import.maven.enabled", false); | ||
json.addProperty("java.import.gradle.enabled", false); | ||
json.addProperty("java.import.gradle.wrapper.enabled", false); | ||
json.addProperty("java.import.gradle.version", gradleVersion); | ||
|
||
JsonArray sourcePathArray = new JsonArray(); | ||
json.add("java.project.sourcePaths", sourcePathArray); | ||
sourcePathArray.add("src/main/java"); | ||
sourcePathArray.add("ghidra_scripts"); | ||
|
||
json.addProperty("java.project.outputPath", "bin/main"); | ||
JsonObject referencedLibrariesObject = new JsonObject(); | ||
|
||
json.add("java.project.referencedLibraries", referencedLibrariesObject); | ||
JsonArray includeArray = new JsonArray(); | ||
referencedLibrariesObject.add("include", includeArray); | ||
classpathSourceMap.keySet().forEach(includeArray::add); | ||
JsonObject sourcesObject = new JsonObject(); | ||
referencedLibrariesObject.add("sources", sourcesObject); | ||
classpathSourceMap.entrySet() | ||
.stream() | ||
.filter(e -> e.getValue() != null) | ||
.forEach(e -> sourcesObject.addProperty(e.getKey(), e.getValue())); | ||
|
||
// Write settings json object | ||
if (!FileUtilities.mkdirs(settingsFile.getParentFile())) { | ||
throw new IOException("Failed to create: " + settingsFile.getParentFile()); | ||
} | ||
FileUtils.writeStringToFile(settingsFile, gson.toJson(json), StandardCharsets.UTF_8); | ||
} | ||
|
||
/** | ||
* Write the .vscode/launch.json file | ||
* | ||
* @param installDir The Ghidra installation directory | ||
* @param projectDir The VSCode project directory | ||
* @param classpathSourceMap The classpath/source map (see {@link #getClasspathSourceMap()}) | ||
* @throws IOException if an IO-related error occurs | ||
*/ | ||
private void writeLaunch(File installDir, File projectDir, | ||
Map<String, String> classpathSourceMap) throws IOException { | ||
File vscodeDir = new File(projectDir, ".vscode"); | ||
File launchFile = new File(vscodeDir, "launch.json"); | ||
|
||
// Get the path of Utility.jar so we can put it on the classpath | ||
String utilityJarPath = classpathSourceMap.keySet() | ||
.stream() | ||
.filter(e -> e.endsWith("Utility.jar")) | ||
.findFirst() | ||
.orElseThrow(); | ||
|
||
// Get JVM args from launch.properties by calling LaunchSupport | ||
List<String> args = new ArrayList<>(); | ||
args.add(System.getProperty("java.home") + "/bin/java"); | ||
args.add("-cp"); | ||
args.add(new File(installDir, "support/LaunchSupport.jar").getPath()); | ||
args.add("LaunchSupport"); | ||
args.add(installDir.getPath()); | ||
args.add("-vmArgs"); | ||
ProcessBuilder pb = new ProcessBuilder(args); | ||
Process p = pb.start(); | ||
List<String> vmArgs = IOUtils.readLines(p.getInputStream(), StandardCharsets.UTF_8); | ||
|
||
// Build launch json object | ||
JsonObject json = new JsonObject(); | ||
json.addProperty("version", "0.2.0"); | ||
JsonArray configurationsArray = new JsonArray(); | ||
json.add("configurations", configurationsArray); | ||
JsonObject ghidraConfigObject = new JsonObject(); | ||
configurationsArray.add(ghidraConfigObject); | ||
ghidraConfigObject.addProperty("type", "java"); | ||
ghidraConfigObject.addProperty("name", "Ghidra"); | ||
ghidraConfigObject.addProperty("request", "launch"); | ||
ghidraConfigObject.addProperty("mainClass", Ghidra.class.getName()); | ||
ghidraConfigObject.addProperty("args", GhidraRun.class.getName()); | ||
JsonArray classPathsArray = new JsonArray(); | ||
ghidraConfigObject.add("classPaths", classPathsArray); | ||
classPathsArray.add(utilityJarPath); | ||
JsonArray vmArgsArray = new JsonArray(); | ||
ghidraConfigObject.add("vmArgs", vmArgsArray); | ||
vmArgsArray.add("-Dghidra.external.modules=${workspaceFolder}"); | ||
vmArgs.forEach(vmArgsArray::add); | ||
|
||
// Write launch json object | ||
if (!FileUtilities.mkdirs(launchFile.getParentFile())) { | ||
throw new IOException("Failed to create: " + launchFile.getParentFile()); | ||
} | ||
FileUtils.writeStringToFile(launchFile, gson.toJson(json), StandardCharsets.UTF_8); | ||
} | ||
|
||
/** | ||
* Write a sample Java-based GhidraScript into the VSCode project directory | ||
* | ||
* @param projectDir The VSCode project directory | ||
* @throws IOException if an IO-related error occurs | ||
*/ | ||
private void writeSampleScriptJava(File projectDir) throws IOException { | ||
File scriptsDir = new File(projectDir, "ghidra_scripts"); | ||
File scriptFile = new File(scriptsDir, "SampleScript.java"); | ||
String sampleScript = """ | ||
// Sample Java GhidraScript | ||
// @category Examples | ||
import ghidra.app.script.GhidraScript; | ||
public class SampleScript extends GhidraScript { | ||
@Override | ||
protected void run() throws Exception { | ||
println(\"Sample script!\"); | ||
} | ||
} | ||
"""; | ||
if (!FileUtilities.mkdirs(scriptFile.getParentFile())) { | ||
throw new IOException("Failed to create: " + scriptFile.getParentFile()); | ||
} | ||
FileUtils.writeStringToFile(scriptFile, sampleScript, StandardCharsets.UTF_8); | ||
} | ||
|
||
/** | ||
* Write a sample Java-based Ghidra module into the VSCode project directory | ||
* | ||
* @param installDir The Ghidra installation directory | ||
* @param projectDir The VSCode project directory | ||
* @throws IOException if an IO-related error occurs | ||
*/ | ||
private void writeSampleModule(File installDir, File projectDir) throws IOException { | ||
// Copy Skeleton and rename module | ||
String skeleton = "Skeleton"; | ||
File skeletonDir = new File(installDir, "Extensions/Ghidra/skeleton"); | ||
FileUtils.copyDirectory(skeletonDir, projectDir); | ||
|
||
// Rename package | ||
String projectName = projectDir.getName(); | ||
File srcDir = new File(projectDir, "src/main/java"); | ||
File oldPackageDir = new File(srcDir, skeleton.toLowerCase()); | ||
File newPackageDir = new File(srcDir, projectName.toLowerCase()); | ||
if (!oldPackageDir.renameTo(newPackageDir)) { | ||
throw new IOException("Failed to rename: " + oldPackageDir); | ||
} | ||
|
||
// Rename java files and text replace their contents | ||
for (File f : newPackageDir.listFiles()) { | ||
String name = f.getName(); | ||
if (!name.startsWith(skeleton)) { | ||
continue; | ||
} | ||
String newName = projectName + name.substring(skeleton.length(), name.length()); | ||
File newFile = new File(f.getParentFile(), newName); | ||
if (!f.renameTo(newFile)) { | ||
throw new IOException("Failed to rename: " + f); | ||
} | ||
String fileData = FileUtils.readFileToString(newFile, StandardCharsets.UTF_8); | ||
fileData = fileData.replaceAll(skeleton, projectName); | ||
fileData = fileData.replaceAll(skeleton.toLowerCase(), projectName.toLowerCase()); | ||
fileData = fileData.replaceAll(skeleton.toUpperCase(), projectName.toUpperCase()); | ||
FileUtils.writeStringToFile(newFile, fileData, StandardCharsets.UTF_8); | ||
} | ||
|
||
// Fix Ghidra installation directory path in build.gradle | ||
File buildGradleFile = new File(projectDir, "build.gradle"); | ||
String fileData = FileUtils.readFileToString(buildGradleFile, StandardCharsets.UTF_8); | ||
fileData = | ||
fileData.replaceAll("<REPLACE>", FilenameUtils.separatorsToUnix(installDir.getPath())); | ||
FileUtils.writeStringToFile(buildGradleFile, fileData, StandardCharsets.UTF_8); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters