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}