package org.eclipse.jdt.internal.launching;
import static org.eclipse.jdt.internal.launching.LaunchingPlugin.LAUNCH_TEMP_FILE_PREFIX;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.URIUtil;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.IStatusHandler;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.jdt.launching.IVMInstall;
import org.eclipse.jdt.launching.IVMInstall2;
public class ClasspathShortener {
private static final String CLASSPATH_ENV_VAR_PREFIX = "CLASSPATH=";
public static final int ARG_MAX_LINUX = 2097152;
public static final int ARG_MAX_WINDOWS = 32767;
public static final int ARG_MAX_MACOS = 262144;
public static final int MAX_ARG_STRLEN_LINUX = 131072;
private final String os;
private final String javaVersion;
private final ILaunch launch;
private final List<String> cmdLine;
private int lastJavaArgumentIndex;
private String[] envp;
private File processTempFilesDir;
private final List<File> processTempFiles = new ArrayList<>();
public ClasspathShortener(IVMInstall vmInstall, ILaunch launch, String[] cmdLine, int lastJavaArgumentIndex, File workingDir, String[] envp) {
this(Platform.getOS(), getJavaVersion(vmInstall), launch, cmdLine, lastJavaArgumentIndex, workingDir, envp);
}
protected ClasspathShortener(String os, String javaVersion, ILaunch launch, String[] cmdLine, int lastJavaArgumentIndex, File workingDir, String[] envp) {
Assert.isNotNull(os);
Assert.isNotNull(javaVersion);
Assert.isNotNull(launch);
Assert.isNotNull(cmdLine);
this.os = os;
this.javaVersion = javaVersion;
this.launch = launch;
this.cmdLine = new ArrayList<>(Arrays.asList(cmdLine));
this.lastJavaArgumentIndex = lastJavaArgumentIndex;
this.envp = envp == null ? null : Arrays.copyOf(envp, envp.length);
this.processTempFilesDir = workingDir != null ? workingDir : Paths.get(".").toAbsolutePath().normalize().toFile();
}
public void setProcessTempFilesDir(File processTempFilesDir) {
this.processTempFilesDir = processTempFilesDir;
}
public File getProcessTempFilesDir() {
return processTempFilesDir;
}
public String[] getEnvp() {
return envp;
}
public String[] getCmdLine() {
return cmdLine.toArray(new String[cmdLine.size()]);
}
public List<File> getProcessTempFiles() {
return new ArrayList<>(processTempFiles);
}
public boolean shortenCommandLineIfNecessary() {
return shortenClasspathIfNecessary() | shortenModulePathIfNecessary();
}
private int getClasspathArgumentIndex() {
for (int i = 0; i <= lastJavaArgumentIndex; i++) {
String element = cmdLine.get(i);
if ("-cp".equals(element) || "-classpath".equals(element) || "--class-path".equals(element)) {
return i + 1;
}
}
return -1;
}
private int getModulepathArgumentIndex() {
for (int i = 0; i <= lastJavaArgumentIndex; i++) {
String element = cmdLine.get(i);
if ("-p".equals(element) || "--module-path".equals(element)) {
return i + 1;
}
}
return -1;
}
private boolean shortenModulePathIfNecessary() {
int modulePathArgumentIndex = getModulepathArgumentIndex();
if (modulePathArgumentIndex == -1) {
return false;
}
try {
String modulePath = cmdLine.get(modulePathArgumentIndex);
if (getCommandLineLength() <= getMaxCommandLineLength() && modulePath.length() <= getMaxArgLength()) {
return false;
}
if (isArgumentFileSupported()) {
shortenModulePathUsingModulePathArgumentFile(modulePathArgumentIndex);
return true;
}
} catch (CoreException e) {
LaunchingPlugin.log(e.getStatus());
}
return false;
}
private boolean shortenClasspathIfNecessary() {
int classpathArgumentIndex = getClasspathArgumentIndex();
if (classpathArgumentIndex == -1) {
return false;
}
try {
boolean forceUseClasspathOnlyJar = getLaunchConfigurationUseClasspathOnlyJarAttribute();
if (forceUseClasspathOnlyJar) {
shortenClasspathUsingClasspathOnlyJar(classpathArgumentIndex);
return true;
}
String classpath = cmdLine.get(classpathArgumentIndex);
if (getCommandLineLength() <= getMaxCommandLineLength() && classpath.length() <= getMaxArgLength()) {
return false;
}
if (isArgumentFileSupported()) {
shortenClasspathUsingClasspathArgumentFile(classpathArgumentIndex);
return true;
}
if (os.equals(Platform.OS_WIN32)) {
shortenClasspathUsingClasspathEnvVariable(classpathArgumentIndex);
return true;
} else if (handleClasspathTooLongStatus()) {
shortenClasspathUsingClasspathOnlyJar(classpathArgumentIndex);
return true;
}
} catch (CoreException e) {
LaunchingPlugin.log(e.getStatus());
}
return false;
}
protected boolean getLaunchConfigurationUseClasspathOnlyJarAttribute() throws CoreException {
ILaunchConfiguration launchConfiguration = launch.getLaunchConfiguration();
if (launchConfiguration == null) {
return false;
}
return launchConfiguration.getAttribute(IJavaLaunchConfigurationConstants.ATTR_USE_CLASSPATH_ONLY_JAR, false);
}
public static String getJavaVersion(IVMInstall vmInstall) {
if (vmInstall instanceof IVMInstall2) {
IVMInstall2 install = (IVMInstall2) vmInstall;
return install.getJavaVersion();
}
return null;
}
private boolean isArgumentFileSupported() {
return JavaCore.compareJavaVersions(javaVersion, JavaCore.VERSION_9) >= 0;
}
private int getCommandLineLength() {
return cmdLine.stream().map(argument -> argument.length() + 1).reduce((a, b) -> a + b).get();
}
private int getEnvironmentLength() {
if (envp == null) {
return 0;
}
return Arrays.stream(envp).map(element -> element.length() + 1).reduce((a, b) -> a + b).orElse(0);
}
protected int getMaxCommandLineLength() {
switch (os) {
case Platform.OS_LINUX:
return ARG_MAX_LINUX - getEnvironmentLength() - 2048;
case Platform.OS_MACOSX:
return ARG_MAX_MACOS - getEnvironmentLength() - 2048;
case Platform.OS_WIN32:
return ARG_MAX_WINDOWS - 2048;
default:
return Integer.MAX_VALUE;
}
}
protected int getMaxArgLength() {
if (os.equals(Platform.OS_LINUX)) {
return MAX_ARG_STRLEN_LINUX - 2048;
}
return Integer.MAX_VALUE;
}
private void shortenClasspathUsingClasspathArgumentFile(int classpathArgumentIndex) throws CoreException {
String classpath = cmdLine.get(classpathArgumentIndex);
File file = createClassPathArgumentFile(classpath);
removeCmdLineArgs(classpathArgumentIndex - 1, 2);
addCmdLineArgs(classpathArgumentIndex - 1, '@' + file.getAbsolutePath());
addProcessTempFile(file);
}
private void shortenModulePathUsingModulePathArgumentFile(int modulePathArgumentIndex) throws CoreException {
String modulePath = cmdLine.get(modulePathArgumentIndex);
File file = createModulePathArgumentFile(modulePath);
removeCmdLineArgs(modulePathArgumentIndex - 1, 2);
addCmdLineArgs(modulePathArgumentIndex - 1, '@' + file.getAbsolutePath());
addProcessTempFile(file);
}
private void shortenClasspathUsingClasspathOnlyJar(int classpathArgumentIndex) throws CoreException {
String classpath = cmdLine.get(classpathArgumentIndex);
File classpathOnlyJar = createClasspathOnlyJar(classpath);
removeCmdLineArgs(classpathArgumentIndex, 1);
addCmdLineArgs(classpathArgumentIndex, classpathOnlyJar.getAbsolutePath());
addProcessTempFile(classpathOnlyJar);
}
protected void addProcessTempFile(File file) {
processTempFiles.add(file);
}
protected boolean handleClasspathTooLongStatus() throws CoreException {
IStatus status = new Status(IStatus.ERROR, LaunchingPlugin.getUniqueIdentifier(), IJavaLaunchConfigurationConstants.ERR_CLASSPATH_TOO_LONG, "", null);
IStatusHandler handler = DebugPlugin.getDefault().getStatusHandler(status);
if (handler == null) {
return false;
}
Object result = handler.handleStatus(status, launch);
if (!(result instanceof Boolean)) {
return false;
}
return (boolean) result;
}
private File createClasspathOnlyJar(String classpath) throws CoreException {
try {
String timeStamp = getLaunchTimeStamp();
File jarFile = new File(processTempFilesDir, String.format(LAUNCH_TEMP_FILE_PREFIX
+ "%s-classpathOnly-%s.jar", getLaunchConfigurationName(), timeStamp));
URI workingDirUri = processTempFilesDir.toURI();
StringBuilder manifestClasspath = new StringBuilder();
String[] classpathArray = getClasspathAsArray(classpath);
for (int i = 0; i < classpathArray.length; i++) {
if (i != 0) {
manifestClasspath.append(' ');
}
File file = new File(classpathArray[i]);
String relativePath = URIUtil.makeRelative(file.toURI(), workingDirUri).toString();
manifestClasspath.append(relativePath);
}
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
manifest.getMainAttributes().put(Attributes.Name.CLASS_PATH, manifestClasspath.toString());
try (JarOutputStream target = new JarOutputStream(new FileOutputStream(jarFile), manifest)) {
}
return jarFile;
} catch (IOException e) {
throw new CoreException(new Status(IStatus.ERROR, LaunchingPlugin.getUniqueIdentifier(), IStatus.ERROR, "Cannot create classpath only jar", e));
}
}
private String[] getClasspathAsArray(String classpath) {
return classpath.split("" + getPathSeparatorChar());
}
protected char getPathSeparatorChar() {
char separator = ':';
if (os.equals(Platform.OS_WIN32)) {
separator = ';';
}
return separator;
}
protected String getLaunchConfigurationName() {
return launch.getLaunchConfiguration().getName();
}
private File createClassPathArgumentFile(String classpath) throws CoreException {
try {
String timeStamp = getLaunchTimeStamp();
File classPathFile = new File(processTempFilesDir, String.format(LAUNCH_TEMP_FILE_PREFIX
+ "%s-classpath-arg-%s.txt", getLaunchConfigurationName(), timeStamp));
byte[] bytes = ("-classpath " + quoteWindowsPath(classpath)).getBytes(StandardCharsets.UTF_8);
Files.write(classPathFile.toPath(), bytes);
return classPathFile;
} catch (IOException e) {
throw new CoreException(new Status(IStatus.ERROR, LaunchingPlugin.getUniqueIdentifier(), IStatus.ERROR, "Cannot create classpath argument file", e));
}
}
private File createModulePathArgumentFile(String modulePath) throws CoreException {
try {
String timeStamp = getLaunchTimeStamp();
File modulePathFile = new File(processTempFilesDir, String.format(LAUNCH_TEMP_FILE_PREFIX
+ "%s-module-path-arg-%s.txt", getLaunchConfigurationName(), timeStamp));
byte[] bytes = ("--module-path " + quoteWindowsPath(modulePath)).getBytes(StandardCharsets.UTF_8);
Files.write(modulePathFile.toPath(), bytes);
return modulePathFile;
} catch (IOException e) {
throw new CoreException(new Status(IStatus.ERROR, LaunchingPlugin.getUniqueIdentifier(), IStatus.ERROR, "Cannot create module-path argument file", e));
}
}
protected String getLaunchTimeStamp() {
String timeStamp = launch.getAttribute(DebugPlugin.ATTR_LAUNCH_TIMESTAMP);
if (timeStamp == null) {
timeStamp = Long.toString(System.currentTimeMillis());
}
return timeStamp;
}
private String[] getEnvpFromNativeEnvironment() {
Map<String, String> nativeEnvironment = getNativeEnvironment();
String[] envp = new String[nativeEnvironment.size()];
int idx = 0;
for (Entry<String, String> entry : nativeEnvironment.entrySet()) {
String value = entry.getValue();
if (value == null) {
value = "";
}
String key = entry.getKey();
envp[idx] = key + '=' + value;
idx++;
}
return envp;
}
protected Map<String, String> getNativeEnvironment() {
return DebugPlugin.getDefault().getLaunchManager().getNativeEnvironment();
}
private void shortenClasspathUsingClasspathEnvVariable(int classpathArgumentIndex) {
String classpath = cmdLine.get(classpathArgumentIndex);
if (envp == null) {
envp = getEnvpFromNativeEnvironment();
}
String classpathEnvVar = CLASSPATH_ENV_VAR_PREFIX + quoteWindowsPath(classpath);
int index = getEnvClasspathIndex(envp);
if (index < 0) {
envp = Arrays.copyOf(envp, envp.length + 1);
envp[envp.length - 1] = classpathEnvVar;
} else {
envp[index] = classpathEnvVar;
}
removeCmdLineArgs(classpathArgumentIndex - 1, 2);
}
private void removeCmdLineArgs(int index, int length) {
for (int i = 0; i < length; i++) {
cmdLine.remove(index);
lastJavaArgumentIndex--;
}
}
private void addCmdLineArgs(int index, String... newArgs) {
cmdLine.addAll(index, Arrays.asList(newArgs));
lastJavaArgumentIndex += newArgs.length;
}
private int getEnvClasspathIndex(String[] env) {
if (env != null) {
for (int i = 0; i < env.length; i++) {
if (env[i].regionMatches(true, 0, CLASSPATH_ENV_VAR_PREFIX, 0, 10)) {
return i;
}
}
}
return -1;
}
public String quoteWindowsPath(String path) {
if (os.equals(Platform.OS_WIN32)) {
return "\"" + path + "\"";
}
return path;
}
}