001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * https://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.bcel.util; 020 021import java.io.Closeable; 022import java.io.File; 023import java.io.FileInputStream; 024import java.io.FilenameFilter; 025import java.io.IOException; 026import java.io.InputStream; 027import java.net.MalformedURLException; 028import java.net.URL; 029import java.nio.file.Files; 030import java.nio.file.Path; 031import java.nio.file.Paths; 032import java.util.ArrayList; 033import java.util.Arrays; 034import java.util.Collections; 035import java.util.Enumeration; 036import java.util.List; 037import java.util.Objects; 038import java.util.StringTokenizer; 039import java.util.stream.Collectors; 040import java.util.zip.ZipEntry; 041import java.util.zip.ZipFile; 042 043import org.apache.bcel.classfile.JavaClass; 044import org.apache.bcel.classfile.Utility; 045import org.apache.commons.io.IOUtils; 046import org.apache.commons.lang3.StringUtils; 047import org.apache.commons.lang3.SystemProperties; 048 049/** 050 * Loads class files from the CLASSPATH. Inspired by sun.tools.ClassPath. 051 */ 052public class ClassPath implements Closeable { 053 054 private abstract static class AbstractPathEntry implements Closeable { 055 056 abstract ClassFile getClassFile(String name, String suffix); 057 058 abstract URL getResource(String name); 059 060 abstract InputStream getResourceAsStream(String name); 061 } 062 063 private abstract static class AbstractZip extends AbstractPathEntry { 064 065 private final ZipFile zipFile; 066 067 AbstractZip(final ZipFile zipFile) { 068 this.zipFile = Objects.requireNonNull(zipFile, "zipFile"); 069 } 070 071 @Override 072 public void close() throws IOException { 073 IOUtils.close(zipFile); 074 } 075 076 @Override 077 ClassFile getClassFile(final String name, final String suffix) { 078 final ZipEntry entry = zipFile.getEntry(toEntryName(name, suffix)); 079 080 if (entry == null) { 081 return null; 082 } 083 084 return new ClassFile() { 085 086 @Override 087 public String getBase() { 088 return zipFile.getName(); 089 } 090 091 @Override 092 public InputStream getInputStream() throws IOException { 093 return zipFile.getInputStream(entry); 094 } 095 096 @Override 097 public String getPath() { 098 return entry.toString(); 099 } 100 101 @Override 102 public long getSize() { 103 return entry.getSize(); 104 } 105 106 @Override 107 public long getTime() { 108 return entry.getTime(); 109 } 110 }; 111 } 112 113 @Override 114 URL getResource(final String name) { 115 final ZipEntry entry = zipFile.getEntry(name); 116 try { 117 return entry != null ? new URL("jar:file:" + zipFile.getName() + "!/" + name) : null; 118 } catch (final MalformedURLException e) { 119 return null; 120 } 121 } 122 123 @Override 124 InputStream getResourceAsStream(final String name) { 125 final ZipEntry entry = zipFile.getEntry(name); 126 try { 127 return entry != null ? zipFile.getInputStream(entry) : null; 128 } catch (final IOException e) { 129 return null; 130 } 131 } 132 133 protected abstract String toEntryName(String name, String suffix); 134 135 @Override 136 public String toString() { 137 return zipFile.getName(); 138 } 139 140 } 141 142 /** 143 * Contains information about file/ZIP entry of the Java class. 144 */ 145 public interface ClassFile { 146 147 /** 148 * Gets the base path of found class. 149 * 150 * @return base path of found class, for example class is contained relative to that path, which may either denote a directory, 151 * or ZIP file. 152 */ 153 String getBase(); 154 155 /** 156 * Gets the input stream for class file. 157 * 158 * @return input stream for class file. 159 * @throws IOException Thrown if an I/O error occurs. 160 */ 161 InputStream getInputStream() throws IOException; 162 163 /** 164 * Gets the canonical path to class file. 165 * 166 * @return canonical path to class file. 167 */ 168 String getPath(); 169 170 /** 171 * Gets the size of class file. 172 * 173 * @return size of class file. 174 */ 175 long getSize(); 176 177 /** 178 * Gets the modification time of class file. 179 * 180 * @return modification time of class file. 181 */ 182 long getTime(); 183 } 184 185 private static final class Dir extends AbstractPathEntry { 186 187 private final String dir; 188 189 Dir(final String d) { 190 dir = d; 191 } 192 193 @Override 194 public void close() throws IOException { 195 // Nothing to do 196 197 } 198 199 @Override 200 ClassFile getClassFile(final String name, final String suffix) { 201 final File file = new File(dir + File.separatorChar + name.replace('.', File.separatorChar) + suffix); 202 return file.exists() ? new ClassFile() { 203 204 @Override 205 public String getBase() { 206 return dir; 207 } 208 209 @Override 210 public InputStream getInputStream() throws IOException { 211 return new FileInputStream(file); 212 } 213 214 @Override 215 public String getPath() { 216 try { 217 return file.getCanonicalPath(); 218 } catch (final IOException e) { 219 return null; 220 } 221 } 222 223 @Override 224 public long getSize() { 225 return file.length(); 226 } 227 228 @Override 229 public long getTime() { 230 return file.lastModified(); 231 } 232 } : null; 233 } 234 235 @Override 236 URL getResource(final String name) { 237 // Resource specification uses '/' whatever the platform 238 final File file = toFile(name); 239 try { 240 return file.exists() ? file.toURI().toURL() : null; 241 } catch (final MalformedURLException e) { 242 return null; 243 } 244 } 245 246 @Override 247 InputStream getResourceAsStream(final String name) { 248 // Resource specification uses '/' whatever the platform 249 final File file = toFile(name); 250 try { 251 return file.exists() ? new FileInputStream(file) : null; 252 } catch (final IOException e) { 253 return null; 254 } 255 } 256 257 private File toFile(final String name) { 258 return new File(dir + File.separatorChar + name.replace('/', File.separatorChar)); 259 } 260 261 @Override 262 public String toString() { 263 return dir; 264 } 265 } 266 267 private static final class Jar extends AbstractZip { 268 269 Jar(final ZipFile zip) { 270 super(zip); 271 } 272 273 @Override 274 protected String toEntryName(final String name, final String suffix) { 275 return Utility.packageToPath(name) + suffix; 276 } 277 278 } 279 280 private static final class JrtModule extends AbstractPathEntry { 281 282 private final Path modulePath; 283 284 JrtModule(final Path modulePath) { 285 this.modulePath = Objects.requireNonNull(modulePath, "modulePath"); 286 } 287 288 @Override 289 public void close() throws IOException { 290 // Nothing to do. 291 292 } 293 294 @Override 295 ClassFile getClassFile(final String name, final String suffix) { 296 final Path resolved = modulePath.resolve(Utility.packageToPath(name) + suffix); 297 if (Files.exists(resolved)) { 298 return new ClassFile() { 299 300 @Override 301 public String getBase() { 302 return Objects.toString(resolved.getFileName(), null); 303 } 304 305 @Override 306 public InputStream getInputStream() throws IOException { 307 return Files.newInputStream(resolved); 308 } 309 310 @Override 311 public String getPath() { 312 return resolved.toString(); 313 } 314 315 @Override 316 public long getSize() { 317 try { 318 return Files.size(resolved); 319 } catch (final IOException e) { 320 return 0; 321 } 322 } 323 324 @Override 325 public long getTime() { 326 try { 327 return Files.getLastModifiedTime(resolved).toMillis(); 328 } catch (final IOException e) { 329 return 0; 330 } 331 } 332 }; 333 } 334 return null; 335 } 336 337 @Override 338 URL getResource(final String name) { 339 final Path resovled = modulePath.resolve(name); 340 try { 341 return Files.exists(resovled) ? new URL("jrt:" + modulePath + "/" + name) : null; 342 } catch (final MalformedURLException e) { 343 return null; 344 } 345 } 346 347 @Override 348 InputStream getResourceAsStream(final String name) { 349 try { 350 return Files.newInputStream(modulePath.resolve(name)); 351 } catch (final IOException e) { 352 return null; 353 } 354 } 355 356 @Override 357 public String toString() { 358 return modulePath.toString(); 359 } 360 361 } 362 363 private static final class JrtModules extends AbstractPathEntry { 364 365 private final ModularRuntimeImage modularRuntimeImage; 366 private final JrtModule[] modules; 367 368 JrtModules(final String path) throws IOException { 369 this.modularRuntimeImage = new ModularRuntimeImage(); 370 this.modules = modularRuntimeImage.list(path).stream().map(JrtModule::new).toArray(JrtModule[]::new); 371 } 372 373 @Override 374 public void close() throws IOException { 375 if (modules != null) { 376 // don't use a for each loop to avoid creating an iterator for the GC to collect. 377 for (final JrtModule module : modules) { 378 module.close(); 379 } 380 } 381 if (modularRuntimeImage != null) { 382 modularRuntimeImage.close(); 383 } 384 } 385 386 @Override 387 ClassFile getClassFile(final String name, final String suffix) { 388 // don't use a for each loop to avoid creating an iterator for the GC to collect. 389 for (final JrtModule module : modules) { 390 final ClassFile classFile = module.getClassFile(name, suffix); 391 if (classFile != null) { 392 return classFile; 393 } 394 } 395 return null; 396 } 397 398 @Override 399 URL getResource(final String name) { 400 // don't use a for each loop to avoid creating an iterator for the GC to collect. 401 for (final JrtModule module : modules) { 402 final URL url = module.getResource(name); 403 if (url != null) { 404 return url; 405 } 406 } 407 return null; 408 } 409 410 @Override 411 InputStream getResourceAsStream(final String name) { 412 // don't use a for each loop to avoid creating an iterator for the GC to collect. 413 for (final JrtModule module : modules) { 414 final InputStream inputStream = module.getResourceAsStream(name); 415 if (inputStream != null) { 416 return inputStream; 417 } 418 } 419 return null; 420 } 421 422 @Override 423 public String toString() { 424 return Arrays.toString(modules); 425 } 426 427 } 428 429 private static final class Module extends AbstractZip { 430 431 Module(final ZipFile zip) { 432 super(zip); 433 } 434 435 @Override 436 protected String toEntryName(final String name, final String suffix) { 437 return "classes/" + Utility.packageToPath(name) + suffix; 438 } 439 440 } 441 442 /** Filter for archive files (.zip and .jar). */ 443 private static final FilenameFilter ARCHIVE_FILTER = (dir, name) -> { 444 name = StringUtils.toRootLowerCase(name); 445 return name.endsWith(".zip") || name.endsWith(".jar"); 446 }; 447 448 /** Filter for module files. */ 449 private static final FilenameFilter MODULES_FILTER = (dir, name) -> { 450 name = StringUtils.toRootLowerCase(name); 451 return name.endsWith(org.apache.bcel.classfile.Module.EXTENSION); 452 }; 453 454 /** The system class path. */ 455 public static final ClassPath SYSTEM_CLASS_PATH = new ClassPath(getClassPath()); 456 457 private static void addJdkModules(final String javaHome, final List<String> list) { 458 String modulesPath = SystemProperties.getJdkModulePath(); 459 if (modulesPath == null || modulesPath.trim().isEmpty()) { 460 // Default to looking in JAVA_HOME/jmods 461 modulesPath = javaHome + File.separator + "jmods"; 462 } 463 final File modulesDir = new File(modulesPath); 464 if (modulesDir.exists()) { 465 final String[] modules = modulesDir.list(MODULES_FILTER); 466 if (modules != null) { 467 for (final String module : modules) { 468 list.add(modulesDir.getPath() + File.separatorChar + module); 469 } 470 } 471 } 472 } 473 474 /** 475 * Checks for class path components in the following properties: "java.class.path", "sun.boot.class.path", 476 * "java.ext.dirs" 477 * 478 * @return class path as used by default by BCEL. 479 */ 480 // @since 6.0 no longer final 481 public static String getClassPath() { 482 final String classPathProp = SystemProperties.getJavaClassPath(); 483 final String bootClassPathProp = System.getProperty("sun.boot.class.path"); 484 final String extDirs = SystemProperties.getJavaExtDirs(); 485 // System.out.println("java.version = " + System.getProperty("java.version")); 486 // System.out.println("java.class.path = " + classPathProp); 487 // System.out.println("sun.boot.class.path=" + bootClassPathProp); 488 // System.out.println("java.ext.dirs=" + extDirs); 489 final String javaHome = SystemProperties.getJavaHome(); 490 final List<String> list = new ArrayList<>(); 491 492 // Starting in JRE 9, .class files are in the modules directory. Add them to the path. 493 final Path modulesPath = Paths.get(javaHome).resolve("lib/modules"); 494 if (Files.exists(modulesPath) && Files.isRegularFile(modulesPath)) { 495 list.add(modulesPath.toAbsolutePath().toString()); 496 } 497 // Starting in JDK 9, .class files are in the jmods directory. Add them to the path. 498 addJdkModules(javaHome, list); 499 500 getPathComponents(classPathProp, list); 501 getPathComponents(bootClassPathProp, list); 502 final List<String> dirs = new ArrayList<>(); 503 getPathComponents(extDirs, dirs); 504 for (final String d : dirs) { 505 final File extDir = new File(d); 506 final String[] extensions = extDir.list(ARCHIVE_FILTER); 507 if (extensions != null) { 508 for (final String extension : extensions) { 509 list.add(extDir.getPath() + File.separatorChar + extension); 510 } 511 } 512 } 513 514 return list.stream().collect(Collectors.joining(File.pathSeparator)); 515 } 516 517 private static void getPathComponents(final String path, final List<String> list) { 518 if (path != null) { 519 final StringTokenizer tokenizer = new StringTokenizer(path, File.pathSeparator); 520 while (tokenizer.hasMoreTokens()) { 521 final String name = tokenizer.nextToken(); 522 final File file = new File(name); 523 if (file.exists()) { 524 list.add(name); 525 } 526 } 527 } 528 } 529 530 private final String classPathString; 531 532 private final ClassPath parent; 533 534 private final List<AbstractPathEntry> paths; 535 536 /** 537 * Search for classes in CLASSPATH. 538 * 539 * @deprecated Use SYSTEM_CLASS_PATH constant 540 */ 541 @Deprecated 542 public ClassPath() { 543 this(getClassPath()); 544 } 545 546 /** 547 * Constructs a ClassPath with a parent and class path string. 548 * 549 * @param parent The parent ClassPath. 550 * @param classPathString The class path string. 551 */ 552 @SuppressWarnings("resource") 553 public ClassPath(final ClassPath parent, final String classPathString) { 554 this.parent = parent; 555 this.classPathString = Objects.requireNonNull(classPathString, "classPathString"); 556 this.paths = new ArrayList<>(); 557 for (final StringTokenizer tokenizer = new StringTokenizer(classPathString, File.pathSeparator); tokenizer.hasMoreTokens();) { 558 final String path = tokenizer.nextToken(); 559 if (!path.isEmpty()) { 560 final File file = new File(path); 561 try { 562 if (file.exists()) { 563 if (file.isDirectory()) { 564 paths.add(new Dir(path)); 565 } else if (path.endsWith(org.apache.bcel.classfile.Module.EXTENSION)) { 566 paths.add(new Module(new ZipFile(file))); 567 } else if (path.endsWith(ModularRuntimeImage.MODULES_PATH)) { 568 paths.add(new JrtModules(ModularRuntimeImage.MODULES_PATH)); 569 } else { 570 paths.add(new Jar(new ZipFile(file))); 571 } 572 } 573 } catch (final IOException e) { 574 if (path.endsWith(".zip") || path.endsWith(".jar")) { 575 System.err.println("CLASSPATH component " + file + ": " + e); 576 } 577 } 578 } 579 } 580 } 581 582 /** 583 * Search for classes in given path. 584 * 585 * @param classPath The class path string. 586 */ 587 public ClassPath(final String classPath) { 588 this(null, classPath); 589 } 590 591 @Override 592 public void close() throws IOException { 593 for (final AbstractPathEntry path : paths) { 594 path.close(); 595 } 596 } 597 598 @Override 599 public boolean equals(final Object obj) { 600 if (this == obj) { 601 return true; 602 } 603 if (obj == null) { 604 return false; 605 } 606 if (getClass() != obj.getClass()) { 607 return false; 608 } 609 final ClassPath other = (ClassPath) obj; 610 return Objects.equals(classPathString, other.classPathString); 611 } 612 613 /** 614 * Gets byte array for the given class. 615 * 616 * @param name fully qualified file name, for example java/lang/String. 617 * @return byte array for class. 618 * @throws IOException Thrown if an I/O error occurs. 619 */ 620 public byte[] getBytes(final String name) throws IOException { 621 return getBytes(name, JavaClass.EXTENSION); 622 } 623 624 /** 625 * Gets byte array for the given file. 626 * 627 * @param name fully qualified file name, for example java/lang/String. 628 * @param suffix file name ends with suffix, for example .java. 629 * @return byte array for file on class path. 630 * @throws IOException Thrown if an I/O error occurs. 631 */ 632 public byte[] getBytes(final String name, final String suffix) throws IOException { 633 try (InputStream inputStream = getInputStream(name, suffix)) { 634 if (inputStream == null) { 635 throw new IOException("Couldn't find: " + name + suffix); 636 } 637 // Read until EOF instead of sizing the buffer from InputStream.available(): for ZIP/JAR entries, available() 638 // reflects the archive's declared uncompressed-size field, which is untrusted metadata. Trusting it lets a 639 // tiny archive force a forged multi-gigabyte allocation, or silently truncate the returned bytes. 640 return IOUtils.toByteArray(inputStream); 641 } 642 } 643 644 /** 645 * Gets the input stream for the given class. 646 * 647 * @param name fully qualified class name, for example {@link String}. 648 * @return input stream for class. 649 * @throws IOException Thrown if an I/O error occurs. 650 */ 651 public ClassFile getClassFile(final String name) throws IOException { 652 return getClassFile(name, JavaClass.EXTENSION); 653 } 654 655 /** 656 * Gets the class file for the given Java class. 657 * 658 * @param name fully qualified file name, for example java/lang/String. 659 * @param suffix file name ends with suffix, for example .java. 660 * @return class file for the Java class. 661 * @throws IOException Thrown if an I/O error occurs. 662 */ 663 public ClassFile getClassFile(final String name, final String suffix) throws IOException { 664 ClassFile cf = null; 665 666 if (parent != null) { 667 cf = parent.getClassFileInternal(name, suffix); 668 } 669 670 if (cf == null) { 671 cf = getClassFileInternal(name, suffix); 672 } 673 674 if (cf != null) { 675 return cf; 676 } 677 678 throw new IOException("Couldn't find: " + name + suffix); 679 } 680 681 private ClassFile getClassFileInternal(final String name, final String suffix) { 682 for (final AbstractPathEntry path : paths) { 683 final ClassFile cf = path.getClassFile(name, suffix); 684 if (cf != null) { 685 return cf; 686 } 687 } 688 return null; 689 } 690 691 /** 692 * Gets an InputStream. 693 * <p> 694 * The caller is responsible for closing the InputStream. 695 * </p> 696 * 697 * @param name fully qualified class name, for example {@link String}. 698 * @return input stream for class. 699 * @throws IOException Thrown if an I/O error occurs. 700 */ 701 public InputStream getInputStream(final String name) throws IOException { 702 return getInputStream(Utility.packageToPath(name), JavaClass.EXTENSION); 703 } 704 705 /** 706 * Gets an InputStream for a class or resource on the classpath. 707 * <p> 708 * The caller is responsible for closing the InputStream. 709 * </p> 710 * 711 * @param name fully qualified file name, for example java/lang/String. 712 * @param suffix file name ends with suff, for example .java. 713 * @return input stream for file on class path. 714 * @throws IOException Thrown if an I/O error occurs. 715 */ 716 public InputStream getInputStream(final String name, final String suffix) throws IOException { 717 try { 718 final java.lang.ClassLoader classLoader = getClass().getClassLoader(); 719 @SuppressWarnings("resource") // closed by caller 720 final 721 InputStream inputStream = classLoader == null ? null : classLoader.getResourceAsStream(name + suffix); 722 if (inputStream != null) { 723 return inputStream; 724 } 725 } catch (final Exception ignored) { 726 // ignored 727 } 728 return getClassFile(name, suffix).getInputStream(); 729 } 730 731 /** 732 * Gets the full canonical path for the given file. 733 * 734 * @param name name of file to search for, for example java/lang/String.java. 735 * @return full (canonical) path for file. 736 * @throws IOException Thrown if an I/O error occurs. 737 */ 738 public String getPath(String name) throws IOException { 739 final int index = name.lastIndexOf('.'); 740 String suffix = ""; 741 if (index > 0) { 742 suffix = name.substring(index); 743 name = name.substring(0, index); 744 } 745 return getPath(name, suffix); 746 } 747 748 /** 749 * Gets the full canonical path for the given file. 750 * 751 * @param name name of file to search for, for example java/lang/String. 752 * @param suffix file name suffix, for example .java. 753 * @return full (canonical) path for file, if it exists. 754 * @throws IOException Thrown if an I/O error occurs. 755 */ 756 public String getPath(final String name, final String suffix) throws IOException { 757 return getClassFile(name, suffix).getPath(); 758 } 759 760 /** 761 * Gets the URL for the given resource. 762 * 763 * @param name fully qualified resource name, for example java/lang/String.class. 764 * @return URL supplying the resource, or null if no resource with that name. 765 * @since 6.0 766 */ 767 public URL getResource(final String name) { 768 for (final AbstractPathEntry path : paths) { 769 final URL url; 770 if ((url = path.getResource(name)) != null) { 771 return url; 772 } 773 } 774 return null; 775 } 776 777 /** 778 * Gets the InputStream for the given resource. 779 * 780 * @param name fully qualified resource name, for example java/lang/String.class. 781 * @return InputStream supplying the resource, or null if no resource with that name. 782 * @since 6.0 783 */ 784 public InputStream getResourceAsStream(final String name) { 785 for (final AbstractPathEntry path : paths) { 786 final InputStream is; 787 if ((is = path.getResourceAsStream(name)) != null) { 788 return is; 789 } 790 } 791 return null; 792 } 793 794 /** 795 * Gets an Enumeration of URLs for the given resource. 796 * 797 * @param name fully qualified resource name, for example java/lang/String.class. 798 * @return An Enumeration of URLs supplying the resource, or an empty Enumeration if no resource with that name. 799 * @since 6.0 800 */ 801 public Enumeration<URL> getResources(final String name) { 802 final List<URL> list = new ArrayList<>(); 803 for (final AbstractPathEntry path : paths) { 804 final URL url; 805 if ((url = path.getResource(name)) != null) { 806 list.add(url); 807 } 808 } 809 return Collections.enumeration(list); 810 } 811 812 @Override 813 public int hashCode() { 814 return classPathString.hashCode(); 815 } 816 817 /** 818 * @return used class path string. 819 */ 820 @Override 821 public String toString() { 822 if (parent != null) { 823 return parent + File.pathSeparator + classPathString; 824 } 825 return classPathString; 826 } 827}