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}