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 * Gets the default class path used by BCEL. 476 * 477 * <p> 478 * Collects class path components from "java.class.path", "sun.boot.class.path", and "java.ext.dirs", along with runtime modules under "java.home". 479 * </p> 480 * 481 * @return class path as used by default by BCEL. 482 */ 483 // @since 6.0 no longer final 484 public static String getClassPath() { 485 final String classPathProp = SystemProperties.getJavaClassPath(); 486 final String bootClassPathProp = System.getProperty("sun.boot.class.path"); 487 final String extDirs = SystemProperties.getJavaExtDirs(); 488 // System.out.println("java.version = " + System.getProperty("java.version")); 489 // System.out.println("java.class.path = " + classPathProp); 490 // System.out.println("sun.boot.class.path=" + bootClassPathProp); 491 // System.out.println("java.ext.dirs=" + extDirs); 492 final String javaHome = SystemProperties.getJavaHome(); 493 final List<String> list = new ArrayList<>(); 494 495 // Starting in JRE 9, .class files are in the modules directory. Add them to the path. 496 final Path modulesPath = Paths.get(javaHome).resolve("lib/modules"); 497 if (Files.exists(modulesPath) && Files.isRegularFile(modulesPath)) { 498 list.add(modulesPath.toAbsolutePath().toString()); 499 } 500 // Starting in JDK 9, .class files are in the jmods directory. Add them to the path. 501 addJdkModules(javaHome, list); 502 503 getPathComponents(classPathProp, list); 504 getPathComponents(bootClassPathProp, list); 505 final List<String> dirs = new ArrayList<>(); 506 getPathComponents(extDirs, dirs); 507 for (final String d : dirs) { 508 final File extDir = new File(d); 509 final String[] extensions = extDir.list(ARCHIVE_FILTER); 510 if (extensions != null) { 511 for (final String extension : extensions) { 512 list.add(extDir.getPath() + File.separatorChar + extension); 513 } 514 } 515 } 516 517 return list.stream().collect(Collectors.joining(File.pathSeparator)); 518 } 519 520 private static void getPathComponents(final String path, final List<String> list) { 521 if (path != null) { 522 final StringTokenizer tokenizer = new StringTokenizer(path, File.pathSeparator); 523 while (tokenizer.hasMoreTokens()) { 524 final String name = tokenizer.nextToken(); 525 final File file = new File(name); 526 if (file.exists()) { 527 list.add(name); 528 } 529 } 530 } 531 } 532 533 private final String classPathString; 534 535 private final ClassPath parent; 536 537 private final List<AbstractPathEntry> paths; 538 539 /** 540 * Search for classes in CLASSPATH. 541 * 542 * @deprecated Use SYSTEM_CLASS_PATH constant 543 */ 544 @Deprecated 545 public ClassPath() { 546 this(getClassPath()); 547 } 548 549 /** 550 * Constructs a ClassPath with a parent and class path string. 551 * 552 * @param parent The parent ClassPath. 553 * @param classPathString The class path string. 554 */ 555 @SuppressWarnings("resource") 556 public ClassPath(final ClassPath parent, final String classPathString) { 557 this.parent = parent; 558 this.classPathString = Objects.requireNonNull(classPathString, "classPathString"); 559 this.paths = new ArrayList<>(); 560 for (final StringTokenizer tokenizer = new StringTokenizer(classPathString, File.pathSeparator); tokenizer.hasMoreTokens();) { 561 final String path = tokenizer.nextToken(); 562 if (!path.isEmpty()) { 563 final File file = new File(path); 564 try { 565 if (file.exists()) { 566 if (file.isDirectory()) { 567 paths.add(new Dir(path)); 568 } else if (path.endsWith(org.apache.bcel.classfile.Module.EXTENSION)) { 569 paths.add(new Module(new ZipFile(file))); 570 } else if (path.endsWith(ModularRuntimeImage.MODULES_PATH)) { 571 paths.add(new JrtModules(ModularRuntimeImage.MODULES_PATH)); 572 } else { 573 paths.add(new Jar(new ZipFile(file))); 574 } 575 } 576 } catch (final IOException e) { 577 if (path.endsWith(".zip") || path.endsWith(".jar")) { 578 System.err.println("CLASSPATH component " + file + ": " + e); 579 } 580 } 581 } 582 } 583 } 584 585 /** 586 * Search for classes in given path. 587 * 588 * @param classPath The class path string. 589 */ 590 public ClassPath(final String classPath) { 591 this(null, classPath); 592 } 593 594 @Override 595 public void close() throws IOException { 596 for (final AbstractPathEntry path : paths) { 597 path.close(); 598 } 599 } 600 601 @Override 602 public boolean equals(final Object obj) { 603 if (this == obj) { 604 return true; 605 } 606 if (obj == null) { 607 return false; 608 } 609 if (getClass() != obj.getClass()) { 610 return false; 611 } 612 final ClassPath other = (ClassPath) obj; 613 return Objects.equals(classPathString, other.classPathString); 614 } 615 616 /** 617 * Gets byte array for the given class. 618 * 619 * @param name fully qualified file name, for example java/lang/String. 620 * @return byte array for class. 621 * @throws IOException Thrown if an I/O error occurs. 622 */ 623 public byte[] getBytes(final String name) throws IOException { 624 return getBytes(name, JavaClass.EXTENSION); 625 } 626 627 /** 628 * Gets byte array for the given file. 629 * 630 * @param name fully qualified file name, for example java/lang/String. 631 * @param suffix file name ends with suffix, for example .java. 632 * @return byte array for file on class path. 633 * @throws IOException Thrown if an I/O error occurs. 634 */ 635 public byte[] getBytes(final String name, final String suffix) throws IOException { 636 try (InputStream inputStream = getInputStream(name, suffix)) { 637 if (inputStream == null) { 638 throw new IOException("Couldn't find: " + name + suffix); 639 } 640 // Read until EOF instead of sizing the buffer from InputStream.available(): for ZIP/JAR entries, available() 641 // reflects the archive's declared uncompressed-size field, which is untrusted metadata. Trusting it lets a 642 // tiny archive force a forged multi-gigabyte allocation, or silently truncate the returned bytes. 643 return IOUtils.toByteArray(inputStream); 644 } 645 } 646 647 /** 648 * Gets the input stream for the given class. 649 * 650 * @param name fully qualified class name, for example {@link String}. 651 * @return input stream for class. 652 * @throws IOException Thrown if an I/O error occurs. 653 */ 654 public ClassFile getClassFile(final String name) throws IOException { 655 return getClassFile(name, JavaClass.EXTENSION); 656 } 657 658 /** 659 * Gets the class file for the given Java class. 660 * 661 * @param name fully qualified file name, for example java/lang/String. 662 * @param suffix file name ends with suffix, for example .java. 663 * @return class file for the Java class. 664 * @throws IOException Thrown if an I/O error occurs. 665 */ 666 public ClassFile getClassFile(final String name, final String suffix) throws IOException { 667 ClassFile cf = null; 668 669 if (parent != null) { 670 cf = parent.getClassFileInternal(name, suffix); 671 } 672 673 if (cf == null) { 674 cf = getClassFileInternal(name, suffix); 675 } 676 677 if (cf != null) { 678 return cf; 679 } 680 681 throw new IOException("Couldn't find: " + name + suffix); 682 } 683 684 private ClassFile getClassFileInternal(final String name, final String suffix) { 685 for (final AbstractPathEntry path : paths) { 686 final ClassFile cf = path.getClassFile(name, suffix); 687 if (cf != null) { 688 return cf; 689 } 690 } 691 return null; 692 } 693 694 /** 695 * Gets an InputStream. 696 * <p> 697 * The caller is responsible for closing the InputStream. 698 * </p> 699 * 700 * @param name fully qualified class name, for example {@link String}. 701 * @return input stream for class. 702 * @throws IOException Thrown if an I/O error occurs. 703 */ 704 public InputStream getInputStream(final String name) throws IOException { 705 return getInputStream(Utility.packageToPath(name), JavaClass.EXTENSION); 706 } 707 708 /** 709 * Gets an InputStream for a class or resource on the classpath. 710 * <p> 711 * The caller is responsible for closing the InputStream. 712 * </p> 713 * 714 * @param name fully qualified file name, for example java/lang/String. 715 * @param suffix file name ends with suff, for example .java. 716 * @return input stream for file on class path. 717 * @throws IOException Thrown if an I/O error occurs. 718 */ 719 public InputStream getInputStream(final String name, final String suffix) throws IOException { 720 try { 721 final java.lang.ClassLoader classLoader = getClass().getClassLoader(); 722 @SuppressWarnings("resource") // closed by caller 723 final 724 InputStream inputStream = classLoader == null ? null : classLoader.getResourceAsStream(name + suffix); 725 if (inputStream != null) { 726 return inputStream; 727 } 728 } catch (final Exception ignored) { 729 // ignored 730 } 731 return getClassFile(name, suffix).getInputStream(); 732 } 733 734 /** 735 * Gets the full canonical path for the given file. 736 * 737 * @param name name of file to search for, for example java/lang/String.java. 738 * @return full (canonical) path for file. 739 * @throws IOException Thrown if an I/O error occurs. 740 */ 741 public String getPath(String name) throws IOException { 742 final int index = name.lastIndexOf('.'); 743 String suffix = ""; 744 if (index > 0) { 745 suffix = name.substring(index); 746 name = name.substring(0, index); 747 } 748 return getPath(name, suffix); 749 } 750 751 /** 752 * Gets the full canonical path for the given file. 753 * 754 * @param name name of file to search for, for example java/lang/String. 755 * @param suffix file name suffix, for example .java. 756 * @return full (canonical) path for file, if it exists. 757 * @throws IOException Thrown if an I/O error occurs. 758 */ 759 public String getPath(final String name, final String suffix) throws IOException { 760 return getClassFile(name, suffix).getPath(); 761 } 762 763 /** 764 * Gets the URL for the given resource. 765 * 766 * @param name fully qualified resource name, for example java/lang/String.class. 767 * @return URL supplying the resource, or null if no resource with that name. 768 * @since 6.0 769 */ 770 public URL getResource(final String name) { 771 for (final AbstractPathEntry path : paths) { 772 final URL url; 773 if ((url = path.getResource(name)) != null) { 774 return url; 775 } 776 } 777 return null; 778 } 779 780 /** 781 * Gets the InputStream for the given resource. 782 * 783 * @param name fully qualified resource name, for example java/lang/String.class. 784 * @return InputStream supplying the resource, or null if no resource with that name. 785 * @since 6.0 786 */ 787 public InputStream getResourceAsStream(final String name) { 788 for (final AbstractPathEntry path : paths) { 789 final InputStream is; 790 if ((is = path.getResourceAsStream(name)) != null) { 791 return is; 792 } 793 } 794 return null; 795 } 796 797 /** 798 * Gets an Enumeration of URLs for the given resource. 799 * 800 * @param name fully qualified resource name, for example java/lang/String.class. 801 * @return An Enumeration of URLs supplying the resource, or an empty Enumeration if no resource with that name. 802 * @since 6.0 803 */ 804 public Enumeration<URL> getResources(final String name) { 805 final List<URL> list = new ArrayList<>(); 806 for (final AbstractPathEntry path : paths) { 807 final URL url; 808 if ((url = path.getResource(name)) != null) { 809 list.add(url); 810 } 811 } 812 return Collections.enumeration(list); 813 } 814 815 @Override 816 public int hashCode() { 817 return classPathString.hashCode(); 818 } 819 820 /** 821 * @return used class path string. 822 */ 823 @Override 824 public String toString() { 825 if (parent != null) { 826 return parent + File.pathSeparator + classPathString; 827 } 828 return classPathString; 829 } 830}