ghidra/GhidraBuild/LaunchSupport/src/main/java/ghidra/launch/JavaConfig.java

429 lines
15 KiB
Java

/* ###
* 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.
*/
package ghidra.launch;
import java.io.*;
import java.text.ParseException;
import java.util.Properties;
import ghidra.launch.JavaFinder.JavaFilter;
/**
* Class to determine and represent a required Java configuration, including minimum and maximum
* supported versions, compiler compliance level, etc.
*/
public class JavaConfig {
private static final String LAUNCH_PROPERTIES_NAME = "launch.properties";
private static final String JAVA_HOME_SAVE_NAME = "java_home.save";
private LaunchProperties launchProperties;
private File javaHomeSaveFile;
private String applicationName; // example: Ghidra
private String applicationVersion; // example: 9.0.1
private String applicationReleaseName; // example: PUBLIC, DEV, etc
private int minSupportedJava;
private int maxSupportedJava;
private String compilerComplianceLevel;
/**
* Creates a new Java configuration for the given installation.
*
* @param installDir The installation directory.
* @throws FileNotFoundException if a required file was not found.
* @throws IOException if there was a problem reading a required file.
* @throws ParseException if there was a problem parsing a required file.
*/
public JavaConfig(File installDir) throws FileNotFoundException, IOException, ParseException {
initApplicationProperties(installDir);
initLaunchProperties(installDir);
initJavaHomeSaveFile(installDir);
}
/**
* Gets the launch properties associated with this Java configuration. Certain aspects of the
* Java configuration are stored in the launch properties.
*
* @return The launch properties associated with this Java configuration. Could be null if
* this Java configuration does not use launch properties.
*/
public LaunchProperties getLaunchProperties() {
return launchProperties;
}
/**
* Gets the Java configuration's minimum supported major Java version.
*
* @return The Java configuration's minimum supported major Java version.
*/
public int getMinSupportedJava() {
return minSupportedJava;
}
/**
* Gets the Java configuration's maximum supported major Java version.
*
* @return The Java configuration's maximum supported major Java version. If there is no
* restriction, the value will be 0.
*/
public int getMaxSupportedJava() {
return maxSupportedJava;
}
/**
* Gets the Java configuration's supported Java architecture. All supported Java
* configurations must have an architecture of <code>64</code>.
*
* @return The Java configuration's supported Java architecture (64).
*/
public int getSupportedArchitecture() {
return 64;
}
/**
* Gets the Java configuration's compiler compliance level that was used to build the
* associated installation.
*
* @return The Java configuration's compiler compliance level.
*/
public String getCompilerComplianceLevel() {
return compilerComplianceLevel;
}
/**
* Gets the Java home directory from the user's Java home save file.
*
* @return The Java home directory from the user's Java home save file, or null if the file
* does not exist or is empty.
* @throws IOException if there was a problem reading the Java home save file.
*/
public File getSavedJavaHome() throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(javaHomeSaveFile))) {
String line = reader.readLine().trim();
if (line != null && !line.isEmpty()) {
return new File(line);
}
}
catch (FileNotFoundException e) {
// Fall through to return null
}
return null;
}
/**
* Saves the given Java home directory to the user's Java home save file. If the save
* file does not exist, it will be created.
*
* @param javaHomeDir The Java home directory to save.
* @return The user's Java home save file.
* @throws IOException if there was a problem saving to the file.
*/
public File saveJavaHome(File javaHomeDir) throws IOException {
if (!javaHomeSaveFile.getParentFile().exists() &&
!javaHomeSaveFile.getParentFile().mkdirs()) {
throw new IOException(
"Failed to create directory: " + javaHomeSaveFile.getParentFile());
}
try (PrintWriter writer = new PrintWriter(new FileWriter(javaHomeSaveFile))) {
writer.println(javaHomeDir);
}
return javaHomeSaveFile;
}
/**
* Tests to see if the given directory is a supported Java home directory for this Java
* configuration.
*
* @param dir The directory to test.
* @param javaFilter A filter used to restrict what kind of Java installations we support.
* @return True if the given directory is a supported Java home directory for this Java
* configuration.
*/
public boolean isSupportedJavaHomeDir(File dir, JavaFilter javaFilter) {
try {
return isJavaVersionSupported(getJavaVersion(dir, javaFilter));
}
catch (IOException | ParseException e) {
return false;
}
}
/**
* Tests to see if the given Java version is supported by this Java launch configuration.
*
* @param javaVersion The java version to check.
* @return True if the given Java version is supported by this Java launch configuration.
*/
public boolean isJavaVersionSupported(JavaVersion javaVersion) {
if (javaVersion.getArchitecture() != getSupportedArchitecture()) {
return false;
}
int major = javaVersion.getMajor();
return major >= minSupportedJava &&
(maxSupportedJava == 0 || major <= maxSupportedJava);
}
/**
* Gets the Java version of the given Java home directory.
*
* @param javaHomeDir The Java home directory to get the version of.
* @param javaFilter A filter used to restrict what kind of Java installations we support.
* @return The Java version of the given Java home directory.
* @throws FileNotFoundException if the given directory is missing a required Java file
* or directory based on the provided filter. The exception's message will have more
* details.
* @throws IOException if there was a problem executing the java executable with the
* "-version" argument.
* @throws ParseException if the version string failed to parse.
*/
public JavaVersion getJavaVersion(File javaHomeDir, JavaFilter javaFilter)
throws FileNotFoundException, IOException, ParseException {
if (javaHomeDir == null) {
throw new FileNotFoundException("Directory not specified");
}
if (!javaHomeDir.isDirectory()) {
throw new FileNotFoundException("Not a directory");
}
File binDir = new File(javaHomeDir, "bin");
if (!binDir.isDirectory()) {
throw new FileNotFoundException("Missing bin directory");
}
File javaExecutable = null;
File javacExecutable = null;
for (File f : binDir.listFiles()) {
if (f.getName().equals("java") || f.getName().equals("java.exe")) {
javaExecutable = f;
}
if (f.getName().equals("javac") || f.getName().equals("javac.exe")) {
javacExecutable = f;
}
}
if (javaExecutable == null) {
throw new FileNotFoundException("Missing java executable");
}
if (javaFilter.equals(JavaFilter.JDK_ONLY) && javacExecutable == null) {
throw new FileNotFoundException("JDK is missing javac executable");
}
if (javaFilter.equals(JavaFilter.JRE_ONLY) && javacExecutable != null) {
throw new FileNotFoundException("JRE should not have javac executable");
}
return runAndGetJavaVersion(javaExecutable);
}
/**
* Gets the version of the given Java executable from the output of running "java -version".
*
* @param javaExecutable The Java executable to run and get the version of.
* @return The version of the given Java executable.
* @throws IOException if there was a problem executing the given Java executable with the
* "-version" argument.
* @throws ParseException if the version string failed to parse.
*/
private JavaVersion runAndGetJavaVersion(File javaExecutable)
throws ParseException, IOException {
String version = "";
String arch = "";
Process proc = Runtime.getRuntime().exec(new String[] { javaExecutable.getAbsolutePath(),
"-XshowSettings:properties", "-version" });
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(proc.getErrorStream()))) {
String line;
while ((version.isEmpty() || arch.isEmpty()) && (line = reader.readLine()) != null) {
line = line.trim();
String searchString = "java.version = ";
if (line.startsWith(searchString)) {
version = line.substring(searchString.length());
}
searchString = "sun.arch.data.model = ";
if (line.startsWith(searchString)) {
arch = line.substring(searchString.length());
}
}
}
if (version.isEmpty()) {
throw new ParseException("Failed to find Java version", 0);
}
if (arch.isEmpty()) {
throw new ParseException("Failed to find Java architecture", 0);
}
return new JavaVersion(version, arch);
}
/**
* Initializes the required application properties for the given installation.
*
* @param installDir The Ghidra installation directory. This is the directory that has the
* "Ghidra" subdirectory in it.
* @throws FileNotFoundException if the application.properties file was not found.
* @throws IOException if there was a problem reading the application.properties file.
* @throws ParseException if there was a problem parsing the required application properties.
*/
private void initApplicationProperties(File installDir) throws FileNotFoundException, IOException, ParseException {
File applicationPropertiesFile = new File(installDir, "Ghidra/application.properties");
if (!applicationPropertiesFile.isFile()) {
throw new FileNotFoundException(
"Application properties file does not exist: " + applicationPropertiesFile);
}
Properties applicationProperties = new Properties();
try (FileInputStream fin = new FileInputStream(applicationPropertiesFile)) {
applicationProperties.load(fin);
}
// Required properties
applicationName = getDefinedProperty(applicationProperties, "application.name");
applicationVersion = getDefinedProperty(applicationProperties, "application.version");
applicationReleaseName =
getDefinedProperty(applicationProperties, "application.release.name");
compilerComplianceLevel =
getDefinedProperty(applicationProperties, "application.java.compiler");
try {
minSupportedJava =
Integer.parseInt(getDefinedProperty(applicationProperties, "application.java.min"));
}
catch (NumberFormatException e) {
throw new ParseException(
"Failed to parse application's minimum supported Java major verison", 0);
}
// Optional properties
String max = applicationProperties.getProperty("application.java.max");
if (max != null && !max.isEmpty()) {
try {
maxSupportedJava = Integer.parseInt(max);
}
catch (NumberFormatException e) {
throw new ParseException(
"Failed to parse application's maximum supported Java major verison", 0);
}
}
else {
maxSupportedJava = 0;
}
}
/**
* Initializes the launch properties for the given installation.
*
* @param installDir The Ghidra installation directory. This is the directory that has the
* "Ghidra" subdirectory in it.
* @throws FileNotFoundException if the given launch properties file does not exist.
* @throws IOException if there was a problem reading the given launch properties file.
* @throws ParseException if there was a problem parsing the given launch properties file.
*/
private void initLaunchProperties(File installDir)
throws FileNotFoundException, IOException, ParseException {
boolean isDev = new File(installDir, "build.gradle").isFile();
// Get the required launch properties file
File launchPropertiesFile = new File(installDir,
(isDev ? "Ghidra/RuntimeScripts/Common/" : "") + "support/" + LAUNCH_PROPERTIES_NAME);
if (!launchPropertiesFile.isFile()) {
throw new FileNotFoundException(
"Launch properties file does not exist: " + launchPropertiesFile);
}
launchProperties = new LaunchProperties(launchPropertiesFile);
}
/**
* Initializes the Java home save file.
*
* @param installDir The Ghidra installation directory. This is the directory that has the
* "Ghidra" subdirectory in it.
* @throws FileNotFoundException if the user's home directory was not found.
*/
private void initJavaHomeSaveFile(File installDir) throws FileNotFoundException {
boolean isDev = new File(installDir, "build.gradle").isFile();
String appName = applicationName.toLowerCase();
String userSettingsDirName = appName + "_" + applicationVersion + "_" +
applicationReleaseName.replaceAll("\\s", "").toUpperCase();
if (isDev) {
userSettingsDirName += "_location_" + installDir.getParentFile().getName();
}
File userSettingsDir = null;
// Look for XDG environment variable
String xdgConfigHomeDirStr = System.getenv("XDG_CONFIG_HOME");
if (xdgConfigHomeDirStr != null && !xdgConfigHomeDirStr.isEmpty()) {
userSettingsDir = new File(xdgConfigHomeDirStr, appName + "/" + userSettingsDirName);
javaHomeSaveFile = new File(userSettingsDir, JAVA_HOME_SAVE_NAME);
return;
}
// Ensure there is a user home directory (there definitely should be)
String userHomeDirPath = System.getProperty("user.home");
if (userHomeDirPath == null || userHomeDirPath.isEmpty()) {
throw new FileNotFoundException("User home directory is not known.");
}
File userHomeDir = new File(userHomeDirPath);
if (!userHomeDir.isDirectory()) {
throw new FileNotFoundException("User home directory does not exist: " + userHomeDir);
}
switch (JavaFinder.getCurrentPlatform()) {
case WINDOWS:
String localAppDataDirPath = System.getenv("APPDATA");
if (localAppDataDirPath == null || localAppDataDirPath.trim().isEmpty()) {
throw new FileNotFoundException("\"APPDATA\" environment variable is not set");
}
userSettingsDir =
new File(localAppDataDirPath, appName + "/" + userSettingsDirName);
break;
case LINUX:
userSettingsDir =
new File(userHomeDir, ".config/" + appName + "/" + userSettingsDirName);
break;
case MACOS:
userSettingsDir =
new File(userHomeDir, "Library/" + appName + "/" + userSettingsDirName);
break;
default:
throw new FileNotFoundException(
"Failed to find the user settings directory: Unsupported operating system.");
}
javaHomeSaveFile = new File(userSettingsDir, JAVA_HOME_SAVE_NAME);
}
/**
* Gets the property value with the given key.
*
* @param properties The properties to get the property from.
* @param key The property's key.
* @return The property's corresponding value.
* @throws ParseException if the property with the given key did not have a defined value.
*/
private String getDefinedProperty(Properties properties, String key) throws ParseException {
String value = properties.getProperty(key);
if (value == null || value.isEmpty()) {
throw new ParseException("Property \"" + key + "\" is not defined.", 0);
}
return value;
}
}