Skip to content

support .jar file to be scanned in cli #259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 1, 2025
Merged
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
22 changes: 17 additions & 5 deletions src/main/java/de/thetaphi/forbiddenapis/Checker.java
Original file line number Diff line number Diff line change
Expand Up @@ -370,11 +370,11 @@ public void setSignaturesSeverity(Collection<String> signatures, ViolationSeveri
forbiddenSignatures.setSignaturesSeverity(signatures, severity);
}

/** Parses and adds a class from the given stream to the list of classes to check. Closes the stream when parsed (on Exception, too)! Does not log anything. */
public void addClassToCheck(final InputStream in, String name) throws IOException {
/** Parses and adds a class from the given stream to the list of classes to check. Does not log anything. */
public void streamReadClassToCheck(final InputStream in, String name) throws IOException {
final ClassReader reader;
try (final InputStream in_ = in) {
reader = AsmUtils.readAndPatchClass(in_);
try {
reader = AsmUtils.readAndPatchClass(in);
} catch (IllegalArgumentException iae) {
throw new IllegalArgumentException(String.format(Locale.ENGLISH,
"The class file format of '%s' is too recent to be parsed by ASM.", name));
Expand All @@ -383,9 +383,21 @@ public void addClassToCheck(final InputStream in, String name) throws IOExceptio
classesToCheck.put(metadata.getBinaryClassName(), metadata);
}

/** Parses and adds a class from the given stream to the list of classes to check. Closes the stream when parsed (on Exception, too)!
* Does not log anything.
* @deprecated Do not use anymore, use {@link #streamReadClassToCheck(InputStream,String)} */
@Deprecated
public void addClassToCheck(final InputStream in, String name) throws IOException {
try (InputStream _in = in) {
streamReadClassToCheck(_in, name);
}
}

/** Parses and adds a class from the given file to the list of classes to check. Does not log anything. */
public void addClassToCheck(File f) throws IOException {
addClassToCheck(new FileInputStream(f), f.toString());
try (InputStream in = new FileInputStream(f)) {
streamReadClassToCheck(in, f.toString());
}
}

/** Parses and adds a multiple class files. */
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/de/thetaphi/forbiddenapis/ant/AntTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Iterator;
Expand Down Expand Up @@ -209,7 +210,9 @@ public void debug(String msg) {
if (restrictClassFilename && name != null && !name.endsWith(".class")) {
continue;
}
checker.addClassToCheck(r.getInputStream(), r.getName());
try (InputStream in = r.getInputStream()) {
checker.streamReadClassToCheck(in, r.getName());
}
foundClass = true;
}
if (!foundClass) {
Expand Down
128 changes: 93 additions & 35 deletions src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,21 @@
import static de.thetaphi.forbiddenapis.Checker.Option.*;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.net.JarURLConnection;
import java.net.URLConnection;
import java.net.URLClassLoader;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.MalformedURLException;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.DefaultParser;
Expand All @@ -38,6 +42,7 @@
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.SelectorUtils;

import de.thetaphi.forbiddenapis.AsmUtils;
import de.thetaphi.forbiddenapis.Checker;
Expand Down Expand Up @@ -68,7 +73,7 @@ public CliMain(String... args) throws ExitException {
final OptionGroup required = new OptionGroup();
required.setRequired(true);
required.addOption(dirOpt = Option.builder("d")
.desc("directory with class files to check for forbidden api usage; this directory is also added to classpath")
.desc("directory (or jar file) with class files to check for forbidden api usage; this directory is also added to classpath")
.longOpt("dir")
.hasArg()
.argName("directory")
Expand Down Expand Up @@ -215,7 +220,7 @@ private void printHelp(Options options) {

public void run() throws ExitException {
final File classesDirectory = new File(cmd.getOptionValue(dirOpt.getLongOpt())).getAbsoluteFile();

// parse classpath given as argument; add -d to classpath, too
final String[] classpath = cmd.getOptionValues(classpathOpt.getLongOpt());
final URL[] urls;
Expand Down Expand Up @@ -260,29 +265,6 @@ public void run() throws ExitException {
checker.addSuppressAnnotation(a);
}

logger.info("Scanning for classes to check...");
if (!classesDirectory.exists()) {
throw new ExitException(EXIT_ERR_OTHER, "Directory with class files does not exist: " + classesDirectory);
}
String[] includes = cmd.getOptionValues(includesOpt.getLongOpt());
if (includes == null || includes.length == 0) {
includes = new String[] { "**/*.class" };
}
final String[] excludes = cmd.getOptionValues(excludesOpt.getLongOpt());
final DirectoryScanner ds = new DirectoryScanner();
ds.setBasedir(classesDirectory);
ds.setCaseSensitive(true);
ds.setIncludes(includes);
ds.setExcludes(excludes);
ds.addDefaultExcludes();
ds.scan();
final String[] files = ds.getIncludedFiles();
if (files.length == 0) {
throw new ExitException(EXIT_ERR_OTHER, String.format(Locale.ENGLISH,
"No classes found in directory %s (includes=%s, excludes=%s).",
classesDirectory, Arrays.toString(includes), Arrays.toString(excludes)));
}

try {
final String[] bundledSignatures = cmd.getOptionValues(bundledsignaturesOpt.getLongOpt());
if (bundledSignatures != null) for (String bs : new LinkedHashSet<>(Arrays.asList(bundledSignatures))) {
Expand Down Expand Up @@ -319,11 +301,75 @@ public void run() throws ExitException {
return;
}
}

logger.info("Scanning for classes to check...");
if (!classesDirectory.exists()) {
throw new ExitException(EXIT_ERR_OTHER, "Directory or zip/jar file with class files does not exist: " + classesDirectory);
}

try {
checker.addClassesToCheck(classesDirectory, files);
} catch (IOException ioe) {
throw new ExitException(EXIT_ERR_OTHER, "Failed to load one of the given class files: " + ioe);
String[] includes = cmd.getOptionValues(includesOpt.getLongOpt());
if (includes == null || includes.length == 0) {
includes = new String[] { "**/*.class" };
}
final String[] excludes = cmd.getOptionValues(excludesOpt.getLongOpt());

if (classesDirectory.isDirectory()) {
final DirectoryScanner ds = new DirectoryScanner();
ds.setBasedir(classesDirectory);
ds.setCaseSensitive(true);
ds.setIncludes(includes);
ds.setExcludes(excludes);
ds.addDefaultExcludes();
ds.scan();
final String[] files = ds.getIncludedFiles();
if (files.length == 0) {
throw new ExitException(EXIT_ERR_OTHER, String.format(Locale.ENGLISH,
"No classes found in directory %s (includes=%s, excludes=%s).",
classesDirectory, Arrays.toString(includes), Arrays.toString(excludes)));
}
try {
checker.addClassesToCheck(classesDirectory, files);
} catch (IOException ioe) {
throw new ExitException(EXIT_ERR_OTHER, "Failed to load one of the given class files: " + ioe);
}
} else if (classesDirectory.getName().matches("(?i).*\\.(zip|jar)")) {
int filesFound = 0;
try (final ZipInputStream zipin = new ZipInputStream(new FileInputStream(classesDirectory))) {
ZipEntry entry;
while ((entry = zipin.getNextEntry()) != null) {
if (entry.isDirectory()) continue;
// ZIP files sometimes contain leading extra slash, remove it after normalization:
final String normalizedName = normalizePath(entry.getName())
.replaceFirst("^" + Pattern.quote(File.separator), "");
next:
for (final String ipattern : includes) {
if (SelectorUtils.matchPath(normalizePattern(ipattern), normalizedName)) {
if (excludes != null) {
for (final String epattern : excludes) {
if (SelectorUtils.matchPath(normalizePattern(epattern), normalizedName)) {
break next;
}
}
}
try {
checker.streamReadClassToCheck(zipin, entry.getName());
filesFound++;
} catch (IOException ioe) {
throw new ExitException(EXIT_ERR_OTHER, String.format(Locale.ENGLISH,
"Failed to load class file '%s' from jar/zip: %s", entry.getName(), ioe));
}
break next;
}
}
}
}
if (filesFound == 0) {
throw new ExitException(EXIT_ERR_OTHER, String.format(Locale.ENGLISH,
"No classes found in jar/zip file %s (includes=%s, excludes=%s).",
classesDirectory, Arrays.toString(includes), Arrays.toString(excludes)));
}
} else {
throw new ExitException(EXIT_ERR_OTHER, "Classes directory parameter is neither a directory or a jar/zip file.");
}

try {
Expand All @@ -336,6 +382,18 @@ public void run() throws ExitException {
}
}

private static String normalizePattern(String pattern) {
pattern = normalizePath(pattern);
if (pattern.endsWith(File.separator)) {
pattern += "**";
}
return pattern;
}

private static String normalizePath(String name) {
return name.trim().replace('/', File.separatorChar).replace('\\', File.separatorChar);
}

public static void main(String... args) {
try {
new CliMain(args).run();
Expand Down
20 changes: 20 additions & 0 deletions src/test/antunit/TestCli.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,24 @@
<au:assertLogContains text="java.lang.String#substring(int,int) [You are crazy that you disallow substrings]"/>
</target>

<target name="testJarInsteadDir">
<java jar="${jar-file}" failonerror="true" fork="true">
<arg value="-c"/>
<arg value="${cp}"/>
<arg value="-d"/>
<arg file="${jar-file}"/>
<arg value="-b"/>
<arg value="jdk-unsafe-${jdk.version},jdk-deprecated-${jdk.version},jdk-non-portable"/>
<arg value="--includes"/>
<arg value="de/thetaphi/forbiddenapis/cli/*.class"/>
<arg value="--excludes"/>
<arg value="**/ExitException.class"/>
</java>
<au:assertLogContains text=" 0 error(s)."/>
<au:assertLogContains text="Scanned 1 class file"/>
<au:assertLogContains text="Reading bundled API signatures: jdk-deprecated-${jdk.version}"/>
<au:assertLogContains text="Reading bundled API signatures: jdk-unsafe-${jdk.version}"/>
<au:assertLogContains text="Reading bundled API signatures: jdk-non-portable"/>
</target>

</project>