Skip to content

Commit 9520c27

Browse files
support .jar file to be scanned in cli (#259)
* support .jar file to be scanned in cli * Rewrite JAR/ZIP file parsing * Fix detection of jar/zip and make the matcher use SelectorUtils#matchPath (like DirectoryScanner) * formatting * Add test --------- Co-authored-by: Uwe Schindler <[email protected]>
1 parent 14c003f commit 9520c27

File tree

4 files changed

+134
-41
lines changed

4 files changed

+134
-41
lines changed

src/main/java/de/thetaphi/forbiddenapis/Checker.java

+17-5
Original file line numberDiff line numberDiff line change
@@ -370,11 +370,11 @@ public void setSignaturesSeverity(Collection<String> signatures, ViolationSeveri
370370
forbiddenSignatures.setSignaturesSeverity(signatures, severity);
371371
}
372372

373-
/** 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. */
374-
public void addClassToCheck(final InputStream in, String name) throws IOException {
373+
/** Parses and adds a class from the given stream to the list of classes to check. Does not log anything. */
374+
public void streamReadClassToCheck(final InputStream in, String name) throws IOException {
375375
final ClassReader reader;
376-
try (final InputStream in_ = in) {
377-
reader = AsmUtils.readAndPatchClass(in_);
376+
try {
377+
reader = AsmUtils.readAndPatchClass(in);
378378
} catch (IllegalArgumentException iae) {
379379
throw new IllegalArgumentException(String.format(Locale.ENGLISH,
380380
"The class file format of '%s' is too recent to be parsed by ASM.", name));
@@ -383,9 +383,21 @@ public void addClassToCheck(final InputStream in, String name) throws IOExceptio
383383
classesToCheck.put(metadata.getBinaryClassName(), metadata);
384384
}
385385

386+
/** Parses and adds a class from the given stream to the list of classes to check. Closes the stream when parsed (on Exception, too)!
387+
* Does not log anything.
388+
* @deprecated Do not use anymore, use {@link #streamReadClassToCheck(InputStream,String)} */
389+
@Deprecated
390+
public void addClassToCheck(final InputStream in, String name) throws IOException {
391+
try (InputStream _in = in) {
392+
streamReadClassToCheck(_in, name);
393+
}
394+
}
395+
386396
/** Parses and adds a class from the given file to the list of classes to check. Does not log anything. */
387397
public void addClassToCheck(File f) throws IOException {
388-
addClassToCheck(new FileInputStream(f), f.toString());
398+
try (InputStream in = new FileInputStream(f)) {
399+
streamReadClassToCheck(in, f.toString());
400+
}
389401
}
390402

391403
/** Parses and adds a multiple class files. */

src/main/java/de/thetaphi/forbiddenapis/ant/AntTask.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import java.io.File;
2424
import java.io.IOException;
25+
import java.io.InputStream;
2526
import java.util.Collection;
2627
import java.util.EnumSet;
2728
import java.util.Iterator;
@@ -209,7 +210,9 @@ public void debug(String msg) {
209210
if (restrictClassFilename && name != null && !name.endsWith(".class")) {
210211
continue;
211212
}
212-
checker.addClassToCheck(r.getInputStream(), r.getName());
213+
try (InputStream in = r.getInputStream()) {
214+
checker.streamReadClassToCheck(in, r.getName());
215+
}
213216
foundClass = true;
214217
}
215218
if (!foundClass) {

src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java

+93-35
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,21 @@
1919
import static de.thetaphi.forbiddenapis.Checker.Option.*;
2020

2121
import java.io.File;
22+
import java.io.FileInputStream;
2223
import java.io.IOException;
24+
import java.net.JarURLConnection;
25+
import java.net.MalformedURLException;
26+
import java.net.URISyntaxException;
27+
import java.net.URL;
28+
import java.net.URLClassLoader;
29+
import java.net.URLConnection;
2330
import java.util.Arrays;
2431
import java.util.EnumSet;
2532
import java.util.LinkedHashSet;
2633
import java.util.Locale;
27-
import java.net.JarURLConnection;
28-
import java.net.URLConnection;
29-
import java.net.URLClassLoader;
30-
import java.net.URISyntaxException;
31-
import java.net.URL;
32-
import java.net.MalformedURLException;
34+
import java.util.regex.Pattern;
35+
import java.util.zip.ZipEntry;
36+
import java.util.zip.ZipInputStream;
3337

3438
import org.apache.commons.cli.CommandLine;
3539
import org.apache.commons.cli.DefaultParser;
@@ -38,6 +42,7 @@
3842
import org.apache.commons.cli.OptionGroup;
3943
import org.apache.commons.cli.Options;
4044
import org.codehaus.plexus.util.DirectoryScanner;
45+
import org.codehaus.plexus.util.SelectorUtils;
4146

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

216221
public void run() throws ExitException {
217222
final File classesDirectory = new File(cmd.getOptionValue(dirOpt.getLongOpt())).getAbsoluteFile();
218-
223+
219224
// parse classpath given as argument; add -d to classpath, too
220225
final String[] classpath = cmd.getOptionValues(classpathOpt.getLongOpt());
221226
final URL[] urls;
@@ -260,29 +265,6 @@ public void run() throws ExitException {
260265
checker.addSuppressAnnotation(a);
261266
}
262267

263-
logger.info("Scanning for classes to check...");
264-
if (!classesDirectory.exists()) {
265-
throw new ExitException(EXIT_ERR_OTHER, "Directory with class files does not exist: " + classesDirectory);
266-
}
267-
String[] includes = cmd.getOptionValues(includesOpt.getLongOpt());
268-
if (includes == null || includes.length == 0) {
269-
includes = new String[] { "**/*.class" };
270-
}
271-
final String[] excludes = cmd.getOptionValues(excludesOpt.getLongOpt());
272-
final DirectoryScanner ds = new DirectoryScanner();
273-
ds.setBasedir(classesDirectory);
274-
ds.setCaseSensitive(true);
275-
ds.setIncludes(includes);
276-
ds.setExcludes(excludes);
277-
ds.addDefaultExcludes();
278-
ds.scan();
279-
final String[] files = ds.getIncludedFiles();
280-
if (files.length == 0) {
281-
throw new ExitException(EXIT_ERR_OTHER, String.format(Locale.ENGLISH,
282-
"No classes found in directory %s (includes=%s, excludes=%s).",
283-
classesDirectory, Arrays.toString(includes), Arrays.toString(excludes)));
284-
}
285-
286268
try {
287269
final String[] bundledSignatures = cmd.getOptionValues(bundledsignaturesOpt.getLongOpt());
288270
if (bundledSignatures != null) for (String bs : new LinkedHashSet<>(Arrays.asList(bundledSignatures))) {
@@ -319,11 +301,75 @@ public void run() throws ExitException {
319301
return;
320302
}
321303
}
304+
305+
logger.info("Scanning for classes to check...");
306+
if (!classesDirectory.exists()) {
307+
throw new ExitException(EXIT_ERR_OTHER, "Directory or zip/jar file with class files does not exist: " + classesDirectory);
308+
}
322309

323-
try {
324-
checker.addClassesToCheck(classesDirectory, files);
325-
} catch (IOException ioe) {
326-
throw new ExitException(EXIT_ERR_OTHER, "Failed to load one of the given class files: " + ioe);
310+
String[] includes = cmd.getOptionValues(includesOpt.getLongOpt());
311+
if (includes == null || includes.length == 0) {
312+
includes = new String[] { "**/*.class" };
313+
}
314+
final String[] excludes = cmd.getOptionValues(excludesOpt.getLongOpt());
315+
316+
if (classesDirectory.isDirectory()) {
317+
final DirectoryScanner ds = new DirectoryScanner();
318+
ds.setBasedir(classesDirectory);
319+
ds.setCaseSensitive(true);
320+
ds.setIncludes(includes);
321+
ds.setExcludes(excludes);
322+
ds.addDefaultExcludes();
323+
ds.scan();
324+
final String[] files = ds.getIncludedFiles();
325+
if (files.length == 0) {
326+
throw new ExitException(EXIT_ERR_OTHER, String.format(Locale.ENGLISH,
327+
"No classes found in directory %s (includes=%s, excludes=%s).",
328+
classesDirectory, Arrays.toString(includes), Arrays.toString(excludes)));
329+
}
330+
try {
331+
checker.addClassesToCheck(classesDirectory, files);
332+
} catch (IOException ioe) {
333+
throw new ExitException(EXIT_ERR_OTHER, "Failed to load one of the given class files: " + ioe);
334+
}
335+
} else if (classesDirectory.getName().matches("(?i).*\\.(zip|jar)")) {
336+
int filesFound = 0;
337+
try (final ZipInputStream zipin = new ZipInputStream(new FileInputStream(classesDirectory))) {
338+
ZipEntry entry;
339+
while ((entry = zipin.getNextEntry()) != null) {
340+
if (entry.isDirectory()) continue;
341+
// ZIP files sometimes contain leading extra slash, remove it after normalization:
342+
final String normalizedName = normalizePath(entry.getName())
343+
.replaceFirst("^" + Pattern.quote(File.separator), "");
344+
next:
345+
for (final String ipattern : includes) {
346+
if (SelectorUtils.matchPath(normalizePattern(ipattern), normalizedName)) {
347+
if (excludes != null) {
348+
for (final String epattern : excludes) {
349+
if (SelectorUtils.matchPath(normalizePattern(epattern), normalizedName)) {
350+
break next;
351+
}
352+
}
353+
}
354+
try {
355+
checker.streamReadClassToCheck(zipin, entry.getName());
356+
filesFound++;
357+
} catch (IOException ioe) {
358+
throw new ExitException(EXIT_ERR_OTHER, String.format(Locale.ENGLISH,
359+
"Failed to load class file '%s' from jar/zip: %s", entry.getName(), ioe));
360+
}
361+
break next;
362+
}
363+
}
364+
}
365+
}
366+
if (filesFound == 0) {
367+
throw new ExitException(EXIT_ERR_OTHER, String.format(Locale.ENGLISH,
368+
"No classes found in jar/zip file %s (includes=%s, excludes=%s).",
369+
classesDirectory, Arrays.toString(includes), Arrays.toString(excludes)));
370+
}
371+
} else {
372+
throw new ExitException(EXIT_ERR_OTHER, "Classes directory parameter is neither a directory or a jar/zip file.");
327373
}
328374

329375
try {
@@ -336,6 +382,18 @@ public void run() throws ExitException {
336382
}
337383
}
338384

385+
private static String normalizePattern(String pattern) {
386+
pattern = normalizePath(pattern);
387+
if (pattern.endsWith(File.separator)) {
388+
pattern += "**";
389+
}
390+
return pattern;
391+
}
392+
393+
private static String normalizePath(String name) {
394+
return name.trim().replace('/', File.separatorChar).replace('\\', File.separatorChar);
395+
}
396+
339397
public static void main(String... args) {
340398
try {
341399
new CliMain(args).run();

src/test/antunit/TestCli.xml

+20
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,24 @@
8686
<au:assertLogContains text="java.lang.String#substring(int,int) [You are crazy that you disallow substrings]"/>
8787
</target>
8888

89+
<target name="testJarInsteadDir">
90+
<java jar="${jar-file}" failonerror="true" fork="true">
91+
<arg value="-c"/>
92+
<arg value="${cp}"/>
93+
<arg value="-d"/>
94+
<arg file="${jar-file}"/>
95+
<arg value="-b"/>
96+
<arg value="jdk-unsafe-${jdk.version},jdk-deprecated-${jdk.version},jdk-non-portable"/>
97+
<arg value="--includes"/>
98+
<arg value="de/thetaphi/forbiddenapis/cli/*.class"/>
99+
<arg value="--excludes"/>
100+
<arg value="**/ExitException.class"/>
101+
</java>
102+
<au:assertLogContains text=" 0 error(s)."/>
103+
<au:assertLogContains text="Scanned 1 class file"/>
104+
<au:assertLogContains text="Reading bundled API signatures: jdk-deprecated-${jdk.version}"/>
105+
<au:assertLogContains text="Reading bundled API signatures: jdk-unsafe-${jdk.version}"/>
106+
<au:assertLogContains text="Reading bundled API signatures: jdk-non-portable"/>
107+
</target>
108+
89109
</project>

0 commit comments

Comments
 (0)