All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.lukehutch.fastclasspathscanner.utils.JarUtils Maven / Gradle / Ivy

Go to download

Uber-fast, ultra-lightweight Java classpath scanner. Scans the classpath by parsing the classfile binary format directly rather than by using reflection. See https://github.com/lukehutch/fast-classpath-scanner

There is a newer version: 4.0.0-beta-7
Show newest version
/*
 * This file is part of FastClasspathScanner.
 *
 * Author: Luke Hutchison
 *
 * Hosted at: https://github.com/lukehutch/fast-classpath-scanner
 *
 * --
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2016 Luke Hutchison
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without
 * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
 * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
 * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
 * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
 * OR OTHER DEALINGS IN THE SOFTWARE.
 */
package io.github.lukehutch.fastclasspathscanner.utils;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class JarUtils {
    /**
     * On everything but Windows, where the path separator is ':', need to treat the colon in these substrings as
     * non-separators, when at the beginning of the string or following a ':'.
     */
    private static final String[] UNIX_NON_PATH_SEPARATORS = { //
            "jar:", "file:", "http://", "https://", //
            // Allow for escaping of ':' characters in paths, which probably goes beyond what the spec would allow
            // for, but would make sense, since File.separatorChar will never be '\\' when File.pathSeparatorChar is
            // ':'
            "\\:" //
    };

    /**
     * The position of the colon characters in the corresponding UNIX_NON_PATH_SEPARATORS array entry.
     */
    private static final int[] UNIX_NON_PATH_SEPARATOR_COLON_POSITIONS;

    static {
        UNIX_NON_PATH_SEPARATOR_COLON_POSITIONS = new int[UNIX_NON_PATH_SEPARATORS.length];
        for (int i = 0; i < UNIX_NON_PATH_SEPARATORS.length; i++) {
            UNIX_NON_PATH_SEPARATOR_COLON_POSITIONS[i] = UNIX_NON_PATH_SEPARATORS[i].indexOf(':');
            if (UNIX_NON_PATH_SEPARATOR_COLON_POSITIONS[i] < 0) {
                throw new RuntimeException("Could not find ':' in \"" + UNIX_NON_PATH_SEPARATORS[i] + "\"");
            }
        }
    }

    /**
     * Split a path on File.pathSeparator (':' on Linux, ';' on Windows), but also allow for the use of URLs with
     * protocol specifiers, e.g. "http://domain/jar1.jar:http://domain/jar2.jar". This is really not even handled by
     * the JRE, in all likelihood, but it's better to be robust.
     */
    public static String[] smartPathSplit(final String pathStr) {
        if (pathStr == null || pathStr.isEmpty()) {
            return new String[0];
        }
        // The fast path for Windows can skips this special handling (no need to handle these cases if the path
        // separator is ';')
        if (File.pathSeparatorChar == ':') {
            // For Linux, don't split on URL protocol boundaries. This will allow for HTTP(S) jars to be given in
            // java.class.path. (The JRE may not even support them, but we may as well do so.)
            final Set splitPoints = new HashSet<>();
            for (int i = -1;;) {
                boolean foundNonPathSeparator = false;
                for (int j = 0; j < UNIX_NON_PATH_SEPARATORS.length; j++) {
                    // Skip ':' characters in the middle of non-path-separators such as "http://"
                    final int startIdx = i - UNIX_NON_PATH_SEPARATOR_COLON_POSITIONS[j];
                    if (pathStr.regionMatches(true, startIdx, UNIX_NON_PATH_SEPARATORS[j], 0,
                            UNIX_NON_PATH_SEPARATORS[j].length())) {
                        // Don't treat the "jar:" in the middle of "x.jar:y.jar" as a URL scheme
                        if (startIdx == 0 || pathStr.charAt(startIdx - 1) == ':') {
                            foundNonPathSeparator = true;
                            break;
                        }
                    }
                }
                if (!foundNonPathSeparator) {
                    // The ':' character is a valid path separator
                    splitPoints.add(i);
                }
                // Search for next ':' character
                i = pathStr.indexOf(':', i + 1);
                if (i < 0) {
                    // Add end of string marker once last ':' has been found
                    splitPoints.add(pathStr.length());
                    break;
                }
            }
            final List splitPointsSorted = new ArrayList<>(splitPoints);
            Collections.sort(splitPointsSorted);
            final List parts = new ArrayList<>();
            for (int i = 1; i < splitPointsSorted.size(); i++) {
                final int idx0 = splitPointsSorted.get(i - 1);
                final int idx1 = splitPointsSorted.get(i);
                // Trim, and unescape "\\:"
                String part = pathStr.substring(idx0 + 1, idx1).trim();
                part = part.replaceAll("\\\\:", ":");
                // Remove empty path components
                if (!part.isEmpty()) {
                    parts.add(part);
                }
            }
            return parts.toArray(new String[parts.size()]);
        } else {
            // For Windows, there is no confusion between the path separator ';' and URL schemes Trim path
            // components, and strip out empty components
            final List partsFiltered = new ArrayList<>();
            for (final String part : pathStr.split(File.pathSeparator)) {
                final String partFiltered = part.trim();
                if (!partFiltered.isEmpty()) {
                    partsFiltered.add(partFiltered);
                }
            }
            return partsFiltered.toArray(new String[partsFiltered.size()]);
        }
    }

    // -------------------------------------------------------------------------------------------------------------

    /** Append a path element to a path string. */
    private static void appendPathElt(final Object pathElt, final StringBuilder buf) {
        if (buf.length() > 0) {
            buf.append(File.pathSeparatorChar);
        }
        // Escape any rogue path separators, as long as file separator is not '\\' (on Windows, if there are any
        // extra ';' characters in a path element, there's really nothing we can do to escape them, since they can't
        // be escaped as "\\;")
        final String path = File.separatorChar == '\\' ? pathElt.toString()
                : pathElt.toString().replaceAll(File.pathSeparator, "\\" + File.pathSeparator);
        buf.append(path);
    }

    /**
     * Get a set of path elements as a string, from an array of objects (e.g. of String, File or URL type, whose
     * toString() method will be called to get the path component), and return the path as a single string
     * delineated with the standard path separator character.
     *
     * @return the delimited path.
     */
    public static String pathElementsToPathStr(final Object... pathElts) {
        final StringBuilder buf = new StringBuilder();
        for (final Object pathElt : pathElts) {
            appendPathElt(pathElt, buf);
        }
        return buf.toString();
    }

    /**
     * Get a set of path elements as a string, from an array of objects (e.g. of String, File or URL type, whose
     * toString() method will be called to get the path component), and return the path as a single string
     * delineated with the standard path separator character.
     *
     * @return the delimited path.
     */
    public static String pathElementsToPathStr(final Iterable pathElts) {
        final StringBuilder buf = new StringBuilder();
        for (final Object pathElt : pathElts) {
            appendPathElt(pathElt, buf);
        }
        return buf.toString();
    }

    // -------------------------------------------------------------------------------------------------------------

    // /** Returns true if the path ends with a jarfile extension, ignoring case. */ public static boolean
    // isJar(final String path) { final int len = path.length(); final boolean isJar = path.regionMatches(true, len
    // - 4, ".jar", 0, 4) // || path.regionMatches(true, len - 4, ".zip", 0, 4) // || path.regionMatches(true, len -
    // 4, ".war", 0, 4) // || path.regionMatches(true, len - 4, ".car", 0, 4) // || path.regionMatches(true, len -
    // 4, ".ear", 0, 4) // || path.regionMatches(true, len - 4, ".sar", 0, 4) // || path.regionMatches(true, len -
    // 4, ".har", 0, 4) // || path.regionMatches(true, len - 4, ".par", 0, 4) // || path.regionMatches(true, len -
    // 6, ".wsjar", 0, 6); if (!isJar) { // Support URLs of the form
    // "http://domain.com/path/to/jarfile.jar?version=2" final int urlParamIdx = path.indexOf('?'); if (urlParamIdx
    // > 0) { return isJar(path.substring(0, urlParamIdx)); } } return isJar; }

    /**
     * Returns the leafname of a path, after first stripping off everything after the first '!', if present.
     */
    public static String leafName(final String path) {
        final int bangIdx = path.indexOf("!");
        final int endIdx = bangIdx >= 0 ? bangIdx : path.length();
        int leafStartIdx = 1 + (File.separatorChar == '/' ? path.lastIndexOf('/', endIdx)
                : Math.max(path.lastIndexOf('/', endIdx), path.lastIndexOf(File.separatorChar, endIdx)));
        // In case of temp files (for jars extracted from within jars), remove the temp filename prefix -- see
        // NestedJarHandler.unzipToTempFile()
        int sepIdx = path.indexOf(NestedJarHandler.TEMP_FILENAME_LEAF_SEPARATOR);
        if (sepIdx >= 0) {
            sepIdx += NestedJarHandler.TEMP_FILENAME_LEAF_SEPARATOR.length();
        }
        leafStartIdx = Math.max(leafStartIdx, sepIdx);
        leafStartIdx = Math.min(leafStartIdx, endIdx);
        return path.substring(leafStartIdx, endIdx);
    }

    // -------------------------------------------------------------------------------------------------------------

    private static final List JRE_JARS = new ArrayList<>();
    private static final Set JRE_JARS_SET = new HashSet<>();
    private static final Set JRE_LIB_JARS = new HashSet<>();
    private static final Set JRE_EXT_JARS = new HashSet<>();

    // Find jars in JRE dirs ({java.home}, {java.home}/lib, {java.home}/lib/ext, etc.)
    static {
        final Set jrePathsSet = new HashSet<>();
        final List jreRtJarPaths = new ArrayList<>();
        final String javaHome = getProperty("java.home");
        if (javaHome != null && !javaHome.isEmpty()) {
            final File javaHomeFile = new File(javaHome);
            addJRERoot(javaHomeFile, jrePathsSet, jreRtJarPaths);
            // Try adding "{java.home}/.." as a JDK root when java.home is a JRE path
            if (javaHomeFile.getName().equals("jre")) {
                addJRERoot(javaHomeFile.getParentFile(), jrePathsSet, jreRtJarPaths);
            } else {
                // Try adding "{java.home}/jre" as a JRE root when java.home is not a JRE path
                addJRERoot(new File(javaHomeFile, "jre"), jrePathsSet, jreRtJarPaths);
            }
            // Add "{java.home}/packages" -- apparently this is used on Solaris, so potentially elsewhere too:
            // https://docs.oracle.com/javase/tutorial/ext/basics/load.html
            addJRERoot(new File(javaHomeFile, "packages"), jrePathsSet, jreRtJarPaths);
        }
        final String javaExtDirs = getProperty("java.ext.dirs");
        final Set javaExtDirsSet = new HashSet<>();
        if (javaExtDirs != null) {
            for (final String javaExtDir : smartPathSplit(javaExtDirs)) {
                if (!javaExtDir.isEmpty()) {
                    addJREPath(new File(javaExtDir), javaExtDirsSet);
                }
            }
        }
        jrePathsSet.addAll(javaExtDirsSet);

        // Add special-case paths for Mac OS X, this is not always picked up from java.home or java.ext.dirs
        addJRERoot(new File("/System/Library/Java"), jrePathsSet, jreRtJarPaths);
        addJRERoot(new File("/System/Library/Java/Libraries"), jrePathsSet, jreRtJarPaths);
        addJRERoot(new File("/System/Library/Java/Extensions"), jrePathsSet, jreRtJarPaths);

        // Add some other site-wide package installation directories (these are prefixes of the typical values for
        // java.ext.dirs, since that only covers the "ext/" dir in this location)
        addJRERoot(new File("/usr/java/packages"), jrePathsSet, jreRtJarPaths);
        addJRERoot(new File("/usr/jdk/packages"), jrePathsSet, jreRtJarPaths);
        try {
            final String systemRoot = File.separatorChar == '\\' ? System.getenv("SystemRoot") : null;
            if (systemRoot != null) {
                addJRERoot(new File(systemRoot, "Sun\\Java"), jrePathsSet, jreRtJarPaths);
                addJRERoot(new File(systemRoot, "Oracle\\Java"), jrePathsSet, jreRtJarPaths);
            }
        } catch (final Exception e) {
        }

        // Find "lib/" and "ext/" jars
        final Set jreJarPaths = new HashSet<>();
        for (final String jrePath : jrePathsSet) {
            final File dir = new File(jrePath);
            if (ClasspathUtils.canRead(dir) && dir.isDirectory()) {
                final boolean isLib = jrePath.endsWith("/lib");
                final boolean isExt = jrePath.endsWith("/ext")
                        // java.ext.dirs dirs may not necessarily end in "/ext"
                        || javaExtDirsSet.contains(jrePath);
                for (final File file : dir.listFiles()) {
                    final String filePath = FastPathResolver.resolve("", file.getPath());
                    if (!filePath.isEmpty()) {
                        if (filePath.endsWith(".jar")) {
                            jreJarPaths.add(filePath);
                            if (isLib) {
                                JRE_LIB_JARS.add(filePath);
                            } else if (isExt) {
                                JRE_EXT_JARS.add(filePath);
                            }
                        }
                    }
                }
            }
        }

        // Put rt.jar first in list of JRE jar paths
        jreJarPaths.removeAll(jreRtJarPaths);
        final List jreJarPathsSorted = new ArrayList<>(jreJarPaths);
        Collections.sort(jreJarPathsSorted);
        if (jreRtJarPaths.size() > 0) {
            // Only include the first rt.jar -- if there is a copy in both the JDK and JRE, no need to scan both
            JRE_JARS.add(jreRtJarPaths.get(0));
        }
        JRE_JARS.addAll(jreJarPathsSorted);
        JRE_JARS_SET.addAll(JRE_JARS);
    }

    private static String getProperty(final String propName) {
        try {
            return System.getProperty(propName);
        } catch (final SecurityException e) {
            return null;
        }
    }

    private static void addJRERoot(final File jreRoot, final Set jrePathsSet,
            final List rtJarPaths) {
        if (addJREPath(jreRoot, jrePathsSet)) {
            final File libFile = new File(jreRoot, "lib");
            if (addJREPath(libFile, jrePathsSet)) {
                final File extFile = new File(libFile, "ext");
                addJREPath(extFile, jrePathsSet);
                final File rtJarFile = new File(libFile, "rt.jar");
                if (ClasspathUtils.canRead(rtJarFile)) {
                    final String rtJarPath = rtJarFile.getPath();
                    if (!rtJarPaths.contains(rtJarPath)) {
                        rtJarPaths.add(rtJarPath);
                    }
                }
            }
        }
    }

    private static boolean addJREPath(final File dir, final Set jrePathsSet) {
        if (ClasspathUtils.canRead(dir) && dir.isDirectory()) {
            String path = dir.getPath();
            if (!path.endsWith(File.separator)) {
                path += File.separator;
            }
            final String jrePath = FastPathResolver.resolve("", path);
            if (!jrePath.isEmpty()) {
                jrePathsSet.add(jrePath);
            }
            try {
                String canonicalPath = dir.getCanonicalPath();
                if (!canonicalPath.endsWith(File.separator)) {
                    canonicalPath += File.separator;
                }
                final String jreCanonicalPath = FastPathResolver.resolve("", canonicalPath);
                if (!jreCanonicalPath.equals(jrePath) && !jreCanonicalPath.isEmpty()) {
                    jrePathsSet.add(jreCanonicalPath);
                }
            } catch (IOException | SecurityException e) {
            }
            return true;
        }
        return false;
    }

    /** Get the paths of jars in all JRE/JDK system directories, with any rt.jar listed first. */
    public static List getJreJarPaths() {
        return JRE_JARS;
    }

    /** Get the paths for any JRE/JDK "lib/" jars. */
    public static Set getJreExtJars() {
        return JRE_EXT_JARS;
    }

    /** Get the paths for any JRE/JDK "ext/" jars. */
    public static Set getJreLibJars() {
        return JRE_LIB_JARS;
    }

    /**
     * Determine whether a given jarfile is in a JRE system directory (jre, jre/lib, jre/lib/ext, etc.).
     */
    public static boolean isJREJar(final String filePath, final Set whitelistedLibOrExtJarPaths,
            final Set blacklistedLibOrExtJarPaths, final LogNode log) {
        if (!whitelistedLibOrExtJarPaths.isEmpty() && whitelistedLibOrExtJarPaths.contains(filePath)
                && !blacklistedLibOrExtJarPaths.contains(filePath)) {
            // This is a whitelisted "lib/" or "ext/" jar, so don't consider this a system jar
            return false;
        }
        return JRE_JARS_SET.contains(filePath);
    }

    /** Prefixes of system (JRE) packages. */
    public static final String[] SYSTEM_PACKAGE_PREFIXES = { //
            "java.", "javax.", "javafx.", "jdk.", "oracle.", "sun." };

    /** Prefixes of system (JRE) packages, turned into path form (with slashes instead of dots). */
    public static final String[] SYSTEM_PACKAGE_PATH_PREFIXES = new String[SYSTEM_PACKAGE_PREFIXES.length];
    static {
        for (int i = 0; i < SYSTEM_PACKAGE_PREFIXES.length; i++) {
            SYSTEM_PACKAGE_PATH_PREFIXES[i] = SYSTEM_PACKAGE_PREFIXES[i].replace('.', '/');
        }
    }

    /** Return true if the given class name, package name or module name has a system package or module prefix */
    public static boolean isInSystemPackageOrModule(final String packageOrModuleName) {
        for (int i = 0; i < SYSTEM_PACKAGE_PREFIXES.length; i++) {
            if (packageOrModuleName.startsWith(SYSTEM_PACKAGE_PREFIXES[i])) {
                return true;
            }
        }
        return false;
    }

    // -------------------------------------------------------------------------------------------------------------

    /**
     * Count the number of bytes before the characters "PK" in a zipfile. Returns -1 if PK is not found anywhere in
     * the file.
     */
    public static long countBytesBeforePKMarker(final File zipfile) throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader(zipfile))) {
            boolean readP = false;
            long fileIdx = 0;
            for (int c; (c = reader.read()) != -1; fileIdx++) {
                if (!readP) {
                    if (c == 'P') {
                        readP = true;
                    }
                } else {
                    if (c == 'K') {
                        // Found PK marker
                        return fileIdx - 1;
                    } else {
                        readP = false;
                    }
                }
            }
            return -1;
        }
    }

    /** Strip the self-extracting archive header from the beginning of a zipfile. */
    public static void stripSFXHeader(final File srcZipfile, final long sfxHeaderBytes, final File destZipfile)
            throws IOException {
        try (FileInputStream inputStream = new FileInputStream(srcZipfile);
                FileChannel inputChannel = inputStream.getChannel();
                FileOutputStream outputStream = new FileOutputStream(destZipfile);
                FileChannel outputChannel = outputStream.getChannel()) {
            inputChannel.position(sfxHeaderBytes);
            outputChannel.transferFrom(inputChannel, 0, inputChannel.size());
        }
    }

    // -------------------------------------------------------------------------------------------------------------

    /** Log the Java version and the JRE paths that were found. */
    public static void logJavaInfo(final LogNode log) {
        if (log != null) {
            log.log("Operating system: " + getProperty("os.name") + " " + getProperty("os.version") + " "
                    + getProperty("os.arch"));
            log.log("Java version: " + getProperty("java.version") + " / " + getProperty("java.runtime.version")
                    + " (" + getProperty("java.vendor") + ")");
            log.log("JRE jars:").log(JRE_JARS);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy