Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 95 additions & 12 deletions base/src/main/java/proguard/InputReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import proguard.classfile.kotlin.KotlinConstants;
import proguard.classfile.util.*;
import proguard.classfile.visitor.*;
Expand All @@ -34,7 +35,11 @@
import proguard.util.*;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static proguard.DataEntryReaderFactory.getFilterExcludingVersionedClasses;

Expand Down Expand Up @@ -231,6 +236,27 @@ public void readInput(String messagePrefix,
}
}

// TODO: For debugging
public static void main(String[] args) throws IOException {
Set<String> withJmods = new HashSet<>();
File file = new File("jdk-25-with-jmods/jmods");
new InputReader(new Configuration()).readInput("message", new ClassPathEntry(file, false), dataEntry -> withJmods.add(dataEntry.getName()));

file = new File("jdk-25-without-jmods/jmods");
Set<String> withoutJmods = new HashSet<>();
new InputReader(new Configuration()).readInput("message", new ClassPathEntry(file, false), dataEntry -> withoutJmods.add(dataEntry.getName()));

Set<String> missing = new HashSet<>(withJmods);
missing.removeAll(withoutJmods);
Set<String> unexpected = new HashSet<>(withoutJmods);
unexpected.removeAll(withJmods);

System.out.println("=== MISSING ===");
missing.forEach(System.out::println);

System.out.println("=== UNEXPECTED ===");
unexpected.forEach(System.out::println);
}

/**
* Reads the given input class path entry.
Expand Down Expand Up @@ -259,18 +285,26 @@ private void readInput(String messagePrefix,
classPathEntry.getName(),
filter != null || classPathEntry.isFiltered() ? " (filtered)" : ""
);

// Create a reader that can unwrap jars, wars, ears, jmods and zips.
DataEntryReader reader =
new DataEntryReaderFactory(configuration.android)
.createDataEntryReader(classPathEntry,
dataEntryReader);

// Create the data entry source.
DataEntrySource source =
new DirectorySource(classPathEntry.getFile());

// Set he feature name for the class files and resource files

File classPathFile = classPathEntry.getFile();
DataEntryReader reader;
DataEntrySource source = maybeGetJrtFallback(classPathFile, classPathEntry.isJmod());
if (source != null) {
reader = dataEntryReader;
logger.info("Using jrt:/ file system fallback due to missing jmods directory: {}", classPathFile);
} else {
// Create a reader that can unwrap jars, wars, ears, jmods and zips.
reader =
new DataEntryReaderFactory(configuration.android)
.createDataEntryReader(classPathEntry,
dataEntryReader);

// Create the data entry source.
source =
new DirectorySource(classPathFile);
}

// Set the feature name for the class files and resource files
// that we'll read.
featureName = classPathEntry.getFeatureName();

Expand All @@ -283,6 +317,55 @@ private void readInput(String messagePrefix,
}
}

/**
* Fallback for JDK >= 24 without {@code jmods} directory, see <a href="https://github.com/Guardsquare/proguard/issues/473">
* issue 473</a>.
*
* <p>If the class path file is detected to be the non-existent {@code JAVA_HOME/jmods} directory or a jmod file
* within it, creates a {@link DataEntrySource} which uses the {@code jrt:/} file system to read JDK classes.
*
* @return a data entry source if the class path file refers to the non-existent {@code JAVA_HOME/jmods} directory,
* or {@code null} otherwise (e.g. does not refer to jmods directory, or jmods directory exists)
*/
@Nullable
private static JrtDataEntrySource maybeGetJrtFallback(File classPathFile, boolean isJmodFile) {
File jmodsDir = classPathFile;
String moduleName = null;

// Handle `jmods/<module>.jmod`
if (isJmodFile) {
jmodsDir = classPathFile.getParentFile();
if (jmodsDir == null) {
return null;
}

String fileName = classPathFile.getName();
moduleName = fileName.substring(0, fileName.lastIndexOf('.'));
}

// First check if this is really a non-existent `JAVA_HOME/jmods` directory
if (!jmodsDir.getName().equals("jmods")) {
return null;
}
if (jmodsDir.exists()) {
return null;
}

Path javaHome = jmodsDir.toPath().getParent();
if (javaHome == null) {
return null;
}

Path jrtFsJarPath = JrtDataEntrySource.getJrtFsJarPath(javaHome);
if (!Files.isRegularFile(jrtFsJarPath)) {
// Not actually a JDK JAVA_HOME (or the `jrt-fs.jar` is missing for whatever reason)
return null;
}

// Now we are sure that this is the jmods directory of a JDK, and that the `jrt:/` file system can be used
return new JrtDataEntrySource(javaHome, moduleName);
}


/**
* This resource file visitor attaches the current resource name, if any,
Expand Down
72 changes: 72 additions & 0 deletions base/src/main/java/proguard/JrtDataEntry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package proguard;

import proguard.io.DataEntry;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;

/**
* Data entry created by {@link JrtDataEntrySource}.
*/
public class JrtDataEntry implements DataEntry {
private final Path jrtPath;
private final String name;
private final long size;

private InputStream inputStream = null;

public JrtDataEntry(Path jrtPath, String name, long size) {
this.jrtPath = jrtPath;
this.name = name;
this.size = size;
}

@Override
public String getName() {
return name;
}

@Override
public String getOriginalName() {
return getName();
}

@Override
public long getSize() {
return size;
}

@Override
public boolean isDirectory() {
return false;
}

@Override
public InputStream getInputStream() throws IOException {
if (inputStream == null) {
inputStream = new BufferedInputStream(Files.newInputStream(jrtPath));
}
return inputStream;
}

@Override
public void closeInputStream() throws IOException {
if (inputStream != null) {
inputStream.close();
inputStream = null;
}
}

@Override
public DataEntry getParent() {
return null;
}

@Override
public String toString() {
return "jrt:/" + jrtPath;
}
}
101 changes: 101 additions & 0 deletions base/src/main/java/proguard/JrtDataEntrySource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package proguard;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import proguard.io.DataEntryReader;
import proguard.io.DataEntrySource;

import java.io.Closeable;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.stream.Stream;

/**
* Data entry source which reads JDK classes using the {@code jrt:/} file system.
*
* @see <a href="https://openjdk.org/jeps/220">JEP 220</a>
*/
public class JrtDataEntrySource implements DataEntrySource, Closeable {
@Nullable
private final String moduleName;
private final FileSystem jrtFileSystem;
private final URLClassLoader fallbackClassLoader;

/**
* @param javaHome path to JAVA_HOME whose classes should be read
* @param moduleName name of the module whose classes should be read; {@code null} to read the classes of
* all modules
*/
public JrtDataEntrySource(Path javaHome, @Nullable String moduleName) {
this.moduleName = moduleName;

Path jrtFsJarPath = getJrtFsJarPath(javaHome);
if (!Files.isRegularFile(jrtFsJarPath)) {
throw new IllegalArgumentException("Missing file in JAVA_HOME: " + jrtFsJarPath);
}
try {
// This class loader is used as fallback when the current JDK is JDK 8 and does not itself have the
// provider for the `jrt:/` file system
fallbackClassLoader = new URLClassLoader(new URL[] {jrtFsJarPath.toUri().toURL()});
} catch (MalformedURLException e) {
throw new RuntimeException("Failed converting file path to URL", e);
}

try {
jrtFileSystem = FileSystems.newFileSystem(URI.create("jrt:/"), Collections.singletonMap("java.home", javaHome.toString()), fallbackClassLoader);
} catch (Exception e) {
// Should not happen normally
throw new RuntimeException("Failed creating jrt:/ file system for JAVA_HOME " + javaHome);
}
}

@Override
public void pumpDataEntries(DataEntryReader dataEntryReader) throws IOException {
Path modulesPath = jrtFileSystem.getPath("modules");
if (moduleName != null) {
Path moduleDir = modulesPath.resolve(moduleName);
if (!Files.isDirectory(moduleDir)) {
throw new IOException("Module '" + moduleName + "' does not exist");
}
processModuleFiles(moduleDir, dataEntryReader);
} else {
try (Stream<Path> moduleDirs = Files.list(modulesPath)) {
for (Path moduleDir : (Iterable<? extends Path>) moduleDirs::iterator) {
processModuleFiles(moduleDir, dataEntryReader);
}
}
}
}

private void processModuleFiles(Path moduleDir, DataEntryReader dataEntryReader) throws IOException {
Files.walkFileTree(moduleDir, new SimpleFileVisitor<Path>() {
@Override
public @NotNull FileVisitResult visitFile(@NotNull Path file, @NotNull BasicFileAttributes attrs) throws IOException {
String name = moduleDir.relativize(file).toString();
dataEntryReader.read(new JrtDataEntry(file, name, attrs.size()));
return FileVisitResult.CONTINUE;
}
});
}

@Override
public void close() throws IOException {
jrtFileSystem.close();
// Note: Closing the class loader might not be thread-safe because by default the JDK caches the underlying
// JarFile, so closing it implicitly here might affect other users of that jar
fallbackClassLoader.close();
}

/**
* Gets the path for the {@code jrt-fs.jar}.
*/
public static Path getJrtFsJarPath(Path javaHome) {
return javaHome.resolve("lib").resolve("jrt-fs.jar");
}
}
Loading