From 2c20a4fa013584dc2b63655de1ea8496a342d08f Mon Sep 17 00:00:00 2001 From: Max Rydahl Andersen Date: Sun, 2 Feb 2025 01:37:26 +0100 Subject: [PATCH 1/5] support .jar file to be scanned in cli --- .../thetaphi/forbiddenapis/cli/CliMain.java | 63 ++++++++++++++++--- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java b/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java index 8e6f275a..59faa41a 100644 --- a/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java +++ b/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java @@ -20,16 +20,19 @@ import java.io.File; import java.io.IOException; -import java.util.Arrays; -import java.util.EnumSet; -import java.util.LinkedHashSet; -import java.util.Locale; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; 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.jar.JarEntry; +import java.util.jar.JarFile; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.DefaultParser; @@ -67,7 +70,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") @@ -196,8 +199,50 @@ private void printHelp(Options options) { } public void run() throws ExitException { - final File classesDirectory = new File(cmd.getOptionValue(dirOpt.getLongOpt())).getAbsoluteFile(); - + File firstClassesDirectory = new File(cmd.getOptionValue(dirOpt.getLongOpt())).getAbsoluteFile(); + + if (!firstClassesDirectory.exists()) { + throw new ExitException(EXIT_ERR_OTHER, "Directory with class files does not exist: " + firstClassesDirectory); + } + + try { + if (!firstClassesDirectory.isDirectory() && firstClassesDirectory.getName().endsWith(".jar")) { + // Create a temporary directory + Path tempDir = Files.createTempDirectory("jar_extract_"); + tempDir.toFile().deleteOnExit(); + System.out.println("Directory is a jar - temporary extracting to " + tempDir); + + // Extract JAR contents + try (JarFile jarFile = new JarFile(firstClassesDirectory)) { + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + Path entryDestination = tempDir.resolve(entry.getName()); + + if (entry.isDirectory()) { + Files.createDirectories(entryDestination); + } else { + Files.createDirectories(entryDestination.getParent()); + try (InputStream in = jarFile.getInputStream(entry); + OutputStream out = Files.newOutputStream(entryDestination)) { + byte[] buffer = new byte[8192]; + int len; + while ((len = in.read(buffer)) > 0) { + out.write(buffer, 0, len); + } + } + } + entryDestination.toFile().deleteOnExit(); + } + } + firstClassesDirectory = tempDir.toFile(); + } + } catch (IOException e) { + throw new ExitException(EXIT_ERR_OTHER, "Could not unpack jar file: " + e); + } + + final File classesDirectory = firstClassesDirectory; + // parse classpath given as argument; add -d to classpath, too final String[] classpath = cmd.getOptionValues(classpathOpt.getLongOpt()); final URL[] urls; @@ -243,9 +288,7 @@ public void run() throws ExitException { } 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" }; From caf139a02866637d03ec90cd9b307dab5df43482 Mon Sep 17 00:00:00 2001 From: Uwe Schindler Date: Tue, 1 Apr 2025 01:13:49 +0200 Subject: [PATCH 2/5] Rewrite JAR/ZIP file parsing --- .../de/thetaphi/forbiddenapis/Checker.java | 22 ++- .../thetaphi/forbiddenapis/ant/AntTask.java | 5 +- .../thetaphi/forbiddenapis/cli/CliMain.java | 156 +++++++++--------- 3 files changed, 99 insertions(+), 84 deletions(-) diff --git a/src/main/java/de/thetaphi/forbiddenapis/Checker.java b/src/main/java/de/thetaphi/forbiddenapis/Checker.java index 734d2e8f..0c9d06e9 100644 --- a/src/main/java/de/thetaphi/forbiddenapis/Checker.java +++ b/src/main/java/de/thetaphi/forbiddenapis/Checker.java @@ -370,11 +370,11 @@ public void setSignaturesSeverity(Collection 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)); @@ -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. */ diff --git a/src/main/java/de/thetaphi/forbiddenapis/ant/AntTask.java b/src/main/java/de/thetaphi/forbiddenapis/ant/AntTask.java index 9a6aba51..3a527556 100644 --- a/src/main/java/de/thetaphi/forbiddenapis/ant/AntTask.java +++ b/src/main/java/de/thetaphi/forbiddenapis/ant/AntTask.java @@ -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; @@ -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) { diff --git a/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java b/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java index fd82634f..360e7e5c 100644 --- a/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java +++ b/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java @@ -19,20 +19,20 @@ import static de.thetaphi.forbiddenapis.Checker.Option.*; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; import java.net.JarURLConnection; -import java.net.URLConnection; -import java.net.URLClassLoader; +import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; -import java.net.MalformedURLException; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; +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.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.DefaultParser; @@ -41,6 +41,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; @@ -217,49 +218,7 @@ private void printHelp(Options options) { } public void run() throws ExitException { - File firstClassesDirectory = new File(cmd.getOptionValue(dirOpt.getLongOpt())).getAbsoluteFile(); - - if (!firstClassesDirectory.exists()) { - throw new ExitException(EXIT_ERR_OTHER, "Directory with class files does not exist: " + firstClassesDirectory); - } - - try { - if (!firstClassesDirectory.isDirectory() && firstClassesDirectory.getName().endsWith(".jar")) { - // Create a temporary directory - Path tempDir = Files.createTempDirectory("jar_extract_"); - tempDir.toFile().deleteOnExit(); - System.out.println("Directory is a jar - temporary extracting to " + tempDir); - - // Extract JAR contents - try (JarFile jarFile = new JarFile(firstClassesDirectory)) { - Enumeration entries = jarFile.entries(); - while (entries.hasMoreElements()) { - JarEntry entry = entries.nextElement(); - Path entryDestination = tempDir.resolve(entry.getName()); - - if (entry.isDirectory()) { - Files.createDirectories(entryDestination); - } else { - Files.createDirectories(entryDestination.getParent()); - try (InputStream in = jarFile.getInputStream(entry); - OutputStream out = Files.newOutputStream(entryDestination)) { - byte[] buffer = new byte[8192]; - int len; - while ((len = in.read(buffer)) > 0) { - out.write(buffer, 0, len); - } - } - } - entryDestination.toFile().deleteOnExit(); - } - } - firstClassesDirectory = tempDir.toFile(); - } - } catch (IOException e) { - throw new ExitException(EXIT_ERR_OTHER, "Could not unpack jar file: " + e); - } - - final File classesDirectory = firstClassesDirectory; + 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()); @@ -305,27 +264,6 @@ public void run() throws ExitException { checker.addSuppressAnnotation(a); } - logger.info("Scanning for classes to check..."); - - 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))) { @@ -362,11 +300,73 @@ public void run() throws ExitException { return; } } + + logger.info("Scanning for classes to check..."); + if (!classesDirectory.exists()) { + throw new ExitException(EXIT_ERR_OTHER, "Directory 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().endsWith(".jar") || classesDirectory.getName().endsWith(".zip")) { + int filesFound = 0; + try (final ZipInputStream zipin = new ZipInputStream(new FileInputStream(classesDirectory))) { + ZipEntry entry; + while ((entry = zipin.getNextEntry()) != null) { + if (entry.isDirectory()) continue; + // cleanup name in the zip file (fix trailing slash and windows separators): + final String name = entry.getName().replace('\\', '/').replaceFirst("^/+", ""); + next: + for (String ipattern : includes) { + if (SelectorUtils.match(ipattern, name)) { + if (excludes != null) { + for (String epattern : excludes) { + if (SelectorUtils.match(epattern, name)) { + break next; + } + } + } + try { + checker.streamReadClassToCheck(zipin, name); + filesFound++; + } catch (IOException ioe) { + throw new ExitException(EXIT_ERR_OTHER, String.format(Locale.ENGLISH, "Failed to load class file '%s' from jar/zip: %s", name, 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 { From dc8a7159a616fcbe97a2279c37c0bb41af817a5b Mon Sep 17 00:00:00 2001 From: Uwe Schindler Date: Tue, 1 Apr 2025 14:46:54 +0200 Subject: [PATCH 3/5] Fix detection of jar/zip and make the matcher use SelectorUtils#matchPath (like DirectoryScanner) --- .../thetaphi/forbiddenapis/cli/CliMain.java | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java b/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java index 360e7e5c..222834ec 100644 --- a/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java +++ b/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java @@ -31,6 +31,7 @@ import java.util.EnumSet; import java.util.LinkedHashSet; import java.util.Locale; +import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -303,7 +304,7 @@ public void run() throws ExitException { logger.info("Scanning for classes to check..."); if (!classesDirectory.exists()) { - throw new ExitException(EXIT_ERR_OTHER, "Directory with class files does not exist: " + classesDirectory); + throw new ExitException(EXIT_ERR_OTHER, "Directory or zip/jar file with class files does not exist: " + classesDirectory); } String[] includes = cmd.getOptionValues(includesOpt.getLongOpt()); @@ -331,29 +332,29 @@ public void run() throws ExitException { } catch (IOException ioe) { throw new ExitException(EXIT_ERR_OTHER, "Failed to load one of the given class files: " + ioe); } - } else if (classesDirectory.getName().endsWith(".jar") || classesDirectory.getName().endsWith(".zip")) { + } 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; - // cleanup name in the zip file (fix trailing slash and windows separators): - final String name = entry.getName().replace('\\', '/').replaceFirst("^/+", ""); + // ZIP files sometimes contain leading extra slash, remove it after normalization: + final String normalizedName = normalizePath(entry.getName()).replaceFirst("^" + Pattern.quote(File.separator), ""); next: - for (String ipattern : includes) { - if (SelectorUtils.match(ipattern, name)) { + for (final String ipattern : includes) { + if (SelectorUtils.matchPath(normalizePattern(ipattern), normalizedName)) { if (excludes != null) { - for (String epattern : excludes) { - if (SelectorUtils.match(epattern, name)) { + for (final String epattern : excludes) { + if (SelectorUtils.matchPath(normalizePattern(epattern), normalizedName)) { break next; } } } try { - checker.streamReadClassToCheck(zipin, name); + 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", name, 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; } @@ -379,6 +380,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(); From 1cc2708fbb10d510724bb362d23364086eda442f Mon Sep 17 00:00:00 2001 From: Uwe Schindler Date: Tue, 1 Apr 2025 14:48:06 +0200 Subject: [PATCH 4/5] formatting --- src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java b/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java index 222834ec..5724c108 100644 --- a/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java +++ b/src/main/java/de/thetaphi/forbiddenapis/cli/CliMain.java @@ -339,7 +339,8 @@ public void run() throws ExitException { 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), ""); + final String normalizedName = normalizePath(entry.getName()) + .replaceFirst("^" + Pattern.quote(File.separator), ""); next: for (final String ipattern : includes) { if (SelectorUtils.matchPath(normalizePattern(ipattern), normalizedName)) { @@ -354,7 +355,8 @@ public void run() throws ExitException { 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)); + 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; } From 19434d9e5c9c87ad85052ac7df0f5ca527d7ae05 Mon Sep 17 00:00:00 2001 From: Uwe Schindler Date: Tue, 1 Apr 2025 15:03:54 +0200 Subject: [PATCH 5/5] Add test --- src/test/antunit/TestCli.xml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/test/antunit/TestCli.xml b/src/test/antunit/TestCli.xml index 4f3d6721..c02d5b29 100644 --- a/src/test/antunit/TestCli.xml +++ b/src/test/antunit/TestCli.xml @@ -86,4 +86,24 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file