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

com.hazelcast.shaded.nonapi.io.github.classgraph.fastzipfilereader.LogicalZipFile Maven / Gradle / Ivy

The newest version!
/*
 * This file is part of ClassGraph.
 *
 * Author: Luke Hutchison
 *
 * Hosted at: https://github.com/classgraph/classgraph
 *
 * --
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2019 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 com.hazelcast.shaded.nonapi.io.github.classgraph.fastzipfilereader;

import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import com.hazelcast.shaded.nonapi.io.github.classgraph.fileslice.ArraySlice;
import com.hazelcast.shaded.nonapi.io.github.classgraph.fileslice.reader.RandomAccessReader;
import com.hazelcast.shaded.nonapi.io.github.classgraph.utils.CollectionUtils;
import com.hazelcast.shaded.nonapi.io.github.classgraph.utils.FileUtils;
import com.hazelcast.shaded.nonapi.io.github.classgraph.utils.LogNode;
import com.hazelcast.shaded.nonapi.io.github.classgraph.utils.StringUtils;
import com.hazelcast.shaded.nonapi.io.github.classgraph.utils.VersionFinder;

/**
 * A logical zipfile, which represents a zipfile contained within a ZipFileSlice of a PhysicalZipFile.
 */
public class LogicalZipFile extends ZipFileSlice {
    /** The zipfile entries. */
    public List entries;

    /** If true, this is a multi-release jar. */
    private boolean isMultiReleaseJar;

    /** A set of classpath roots found in the classpath for this zipfile. */
    Set classpathRoots = Collections.newSetFromMap(new ConcurrentHashMap());

    /** The value of the "Class-Path" manifest entry, if present in the manifest, else null. */
    public String classPathManifestEntryValue;

    /** The value of the "Bundle-ClassPath" manifest entry, if present in the manifest, else null. */
    public String bundleClassPathManifestEntryValue;

    /** The value of the "Add-Exports" manifest entry, if present in the manifest, else null. */
    public String addExportsManifestEntryValue;

    /** The value of the "Add-Opens" manifest entry, if present in the manifest, else null. */
    public String addOpensManifestEntryValue;

    /** The value of the "Automatic-Module-Name" manifest entry, if present in the manifest, else null. */
    public String automaticModuleNameManifestEntryValue;

    /** If true, this is a JRE jar. */
    public boolean isJREJar;

    /** If true, multi-release versions should not be stripped in resource names. */
    private final boolean enableMultiReleaseVersions;

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

    /** {@code "META_INF/"}. */
    static final String META_INF_PATH_PREFIX = "META-INF/";

    /** {@code "META-INF/MANIFEST.MF"}. */
    private static final String MANIFEST_PATH = META_INF_PATH_PREFIX + "MANIFEST.MF";

    /** {@code "META-INF/versions/"}. */
    public static final String MULTI_RELEASE_PATH_PREFIX = META_INF_PATH_PREFIX + "versions/";

    /** The {@code "Implementation-Title"} manifest key. */
    private static final byte[] IMPLEMENTATION_TITLE_KEY = manifestKeyToBytes("Implementation-Title");

    /** The {@code "Specification-Title"} manifest key. */
    private static final byte[] SPECIFICATION_TITLE_KEY = manifestKeyToBytes("Specification-Title");

    /** The {@code "Class-Path"} manifest key. */
    private static final byte[] CLASS_PATH_KEY = manifestKeyToBytes("Class-Path");

    /** The {@code "Bundle-ClassPath"} manifest key. */
    private static final byte[] BUNDLE_CLASSPATH_KEY = manifestKeyToBytes("Bundle-ClassPath");

    /** The {@code "Spring-Boot-Classes"} manifest key. */
    private static final byte[] SPRING_BOOT_CLASSES_KEY = manifestKeyToBytes("Spring-Boot-Classes");

    /** The {@code "Spring-Boot-Lib"} manifest key. */
    private static final byte[] SPRING_BOOT_LIB_KEY = manifestKeyToBytes("Spring-Boot-Lib");

    /** The {@code "Multi-Release"} manifest key. */
    private static final byte[] MULTI_RELEASE_KEY = manifestKeyToBytes("Multi-Release");

    /** The {@code "Add-Exports"} manifest key. */
    private static final byte[] ADD_EXPORTS_KEY = manifestKeyToBytes("Add-Exports");

    /** The {@code "Add-Opens"} manifest key. */
    private static final byte[] ADD_OPENS_KEY = manifestKeyToBytes("Add-Opens");

    /** The {@code "Automatic-Module-Name"} manifest key. */
    private static final byte[] AUTOMATIC_MODULE_NAME_KEY = manifestKeyToBytes("Automatic-Module-Name");

    /** For quickly converting ASCII characters to lower case. */
    private static byte[] toLowerCase = new byte[256];
    static {
        for (int i = 32; i < 127; i++) {
            toLowerCase[i] = (byte) Character.toLowerCase((char) i);
        }
    }

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

    /**
     * Construct a logical zipfile from a slice of a physical zipfile.
     *
     * @param zipFileSlice
     *            the zipfile slice
     * @param nestedJarHandler
     *            the nested jar handler
     * @param log
     *            the log
     * @throws IOException
     *             If an I/O exception occurs.
     * @throws InterruptedException
     *             if the thread was interrupted.
     */
    LogicalZipFile(final ZipFileSlice zipFileSlice, final NestedJarHandler nestedJarHandler, final LogNode log,
            final boolean enableMultiReleaseVersions) throws IOException, InterruptedException {
        super(zipFileSlice);
        this.enableMultiReleaseVersions = enableMultiReleaseVersions;
        readCentralDirectory(nestedJarHandler, log);
    }

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

    /**
     * Extract a value from the manifest, and return the value as a string, along with the index after the
     * terminating newline. Manifest files support three different line terminator types, and entries can be split
     * across lines with a line terminator followed by a space.
     *
     * @param manifest
     *            the manifest bytes
     * @param startIdx
     *            the start index of the manifest value
     * @return the manifest value
     */
    private static Entry getManifestValue(final byte[] manifest, final int startIdx) {
        // See if manifest entry is split across multiple lines
        int curr = startIdx;
        final int len = manifest.length;
        while (curr < len && manifest[curr] == (byte) ' ') {
            // Skip initial spaces
            curr++;
        }
        final int firstNonSpaceIdx = curr;
        boolean isMultiLine = false;
        for (; curr < len && !isMultiLine; curr++) {
            final byte b = manifest[curr];
            if (b == (byte) '\r' && curr < len - 1 && manifest[curr + 1] == (byte) '\n') {
                if (curr < len - 2 && manifest[curr + 2] == (byte) ' ') {
                    isMultiLine = true;
                }
                break;
            } else if (b == (byte) '\r' || b == (byte) '\n') {
                if (curr < len - 1 && manifest[curr + 1] == (byte) ' ') {
                    isMultiLine = true;
                }
                break;
            }
        }
        String val;
        if (!isMultiLine) {
            // Fast path for single-line value
            val = new String(manifest, firstNonSpaceIdx, curr - firstNonSpaceIdx, StandardCharsets.UTF_8);
        } else {
            // Skip (newline + space) sequences in multi-line values
            final ByteArrayOutputStream buf = new ByteArrayOutputStream();
            curr = firstNonSpaceIdx;
            for (; curr < len; curr++) {
                final byte b = manifest[curr];
                boolean isLineEnd;
                if (b == (byte) '\r' && curr < len - 1 && manifest[curr + 1] == (byte) '\n') {
                    // CRLF
                    curr += 2;
                    isLineEnd = true;
                } else if (b == '\r' || b == '\n') {
                    // CR or LF
                    curr += 1;
                    isLineEnd = true;
                } else {
                    buf.write(b);
                    isLineEnd = false;
                }
                if (isLineEnd && curr < len && manifest[curr] != (byte) ' ') {
                    // Value ends if line break is not followed by a space
                    break;
                }
                // If line break was followed by a space, then the curr++ in the for loop header will skip it
            }
            try {
                val = buf.toString("UTF-8");
            } catch (final UnsupportedEncodingException e) {
                // Should not happen
                throw new RuntimeException("UTF-8 encoding is not supported in your JRE", e);
            }
        }
        return new SimpleEntry<>(val.endsWith(" ") ? val.trim() : val, curr);
    }

    /**
     * Manifest key to bytes.
     *
     * @param key
     *            the manifest key
     * @return the manifest key bytes, lowercased.
     */
    private static byte[] manifestKeyToBytes(final String key) {
        final byte[] bytes = new byte[key.length()];
        for (int i = 0; i < key.length(); i++) {
            bytes[i] = (byte) Character.toLowerCase(key.charAt(i));
        }
        return bytes;
    }

    /**
     * Key matches at position.
     *
     * @param manifest
     *            the manifest
     * @param key
     *            the key
     * @param pos
     *            the position to try matching
     * @return true if the key matches at this position
     */
    private static boolean keyMatchesAtPosition(final byte[] manifest, final byte[] key, final int pos) {
        if (pos + key.length + 1 > manifest.length || manifest[pos + key.length] != ':') {
            return false;
        }
        for (int i = 0; i < key.length; i++) {
            // Manifest keys are case insensitive
            if (toLowerCase[manifest[i + pos]] != key[i]) {
                return false;
            }
        }
        return true;
    }

    /**
     * Parse the manifest entry of a zipfile.
     *
     * @param manifestZipEntry
     *            the manifest zip entry
     * @param log
     *            the log
     * @throws IOException
     *             If an I/O exception occurs.
     * @throws InterruptedException
     *             If the thread was interrupted.
     */
    private void parseManifest(final FastZipEntry manifestZipEntry, final LogNode log)
            throws IOException, InterruptedException {
        // Load contents of manifest entry as a byte array
        final byte[] manifest = manifestZipEntry.getSlice().load();

        // Find field keys (separated by newlines)
        for (int i = 0; i < manifest.length;) {
            // There cannot be any space after a newline before the manifest key, so key starts immediately
            boolean skip = false;
            if (manifest[i] == (byte) '\n' || manifest[i] == (byte) '\r') {
                // Skip blank lines
                skip = true;

            } else if (keyMatchesAtPosition(manifest, IMPLEMENTATION_TITLE_KEY, i)) {
                final Entry manifestValueAndEndIdx = getManifestValue(manifest,
                        i + IMPLEMENTATION_TITLE_KEY.length + 1);
                if (manifestValueAndEndIdx.getKey().equalsIgnoreCase("Java Runtime Environment")) {
                    isJREJar = true;
                }
                i = manifestValueAndEndIdx.getValue();

            } else if (keyMatchesAtPosition(manifest, SPECIFICATION_TITLE_KEY, i)) {
                final Entry manifestValueAndEndIdx = getManifestValue(manifest,
                        i + SPECIFICATION_TITLE_KEY.length + 1);
                if (manifestValueAndEndIdx.getKey().equalsIgnoreCase("Java Platform API Specification")) {
                    isJREJar = true;
                }
                i = manifestValueAndEndIdx.getValue();

            } else if (keyMatchesAtPosition(manifest, CLASS_PATH_KEY, i)) {
                final Entry manifestValueAndEndIdx = getManifestValue(manifest,
                        i + CLASS_PATH_KEY.length + 1);
                // Add Class-Path manifest entry values to classpath
                classPathManifestEntryValue = manifestValueAndEndIdx.getKey();
                if (log != null) {
                    log.log("Found Class-Path entry in manifest file: " + classPathManifestEntryValue);
                }
                i = manifestValueAndEndIdx.getValue();

            } else if (keyMatchesAtPosition(manifest, BUNDLE_CLASSPATH_KEY, i)) {
                final Entry manifestValueAndEndIdx = getManifestValue(manifest,
                        i + BUNDLE_CLASSPATH_KEY.length + 1);
                // Add Bundle-ClassPath manifest entry values to classpath
                bundleClassPathManifestEntryValue = manifestValueAndEndIdx.getKey();
                if (log != null) {
                    log.log("Found Bundle-ClassPath entry in manifest file: " + bundleClassPathManifestEntryValue);
                }
                i = manifestValueAndEndIdx.getValue();

            } else if (keyMatchesAtPosition(manifest, SPRING_BOOT_CLASSES_KEY, i)) {
                final Entry manifestValueAndEndIdx = getManifestValue(manifest,
                        i + SPRING_BOOT_CLASSES_KEY.length + 1);
                final String springBootClassesFieldVal = manifestValueAndEndIdx.getKey();
                if (!springBootClassesFieldVal.equals("BOOT-INF/classes")
                        && !springBootClassesFieldVal.equals("BOOT-INF/classes/")
                        && !springBootClassesFieldVal.equals("WEB-INF/classes")
                        && !springBootClassesFieldVal.equals("WEB-INF/classes/")) {
                    throw new IOException("Spring boot classes are at \"" + springBootClassesFieldVal
                            + "\" rather than the standard location \"BOOT-INF/classes/\" or \"WEB-INF/classes/\" "
                            + "-- please report this at https://github.com/classgraph/classgraph/issues");
                }
                i = manifestValueAndEndIdx.getValue();

            } else if (keyMatchesAtPosition(manifest, SPRING_BOOT_LIB_KEY, i)) {
                final Entry manifestValueAndEndIdx = getManifestValue(manifest,
                        i + SPRING_BOOT_LIB_KEY.length + 1);
                final String springBootLibFieldVal = manifestValueAndEndIdx.getKey();
                if (!springBootLibFieldVal.equals("BOOT-INF/lib") && !springBootLibFieldVal.equals("BOOT-INF/lib/")
                        && !springBootLibFieldVal.equals("WEB-INF/lib")
                        && !springBootLibFieldVal.equals("WEB-INF/lib/")) {
                    throw new IOException("Spring boot lib jars are at \"" + springBootLibFieldVal
                            + "\" rather than the standard location \"BOOT-INF/lib/\" or \"WEB-INF/lib/\" "
                            + "-- please report this at https://github.com/classgraph/classgraph/issues");
                }
                i = manifestValueAndEndIdx.getValue();

            } else if (keyMatchesAtPosition(manifest, MULTI_RELEASE_KEY, i)) {
                final Entry manifestValueAndEndIdx = getManifestValue(manifest,
                        i + MULTI_RELEASE_KEY.length + 1);
                if (manifestValueAndEndIdx.getKey().equalsIgnoreCase("true")) {
                    isMultiReleaseJar = true;
                }
                i = manifestValueAndEndIdx.getValue();

            } else if (keyMatchesAtPosition(manifest, ADD_EXPORTS_KEY, i)) {
                final Entry manifestValueAndEndIdx = getManifestValue(manifest,
                        i + ADD_EXPORTS_KEY.length + 1);
                addExportsManifestEntryValue = manifestValueAndEndIdx.getKey();
                if (log != null) {
                    log.log("Found Add-Exports entry in manifest file: " + addExportsManifestEntryValue);
                }
                i = manifestValueAndEndIdx.getValue();

            } else if (keyMatchesAtPosition(manifest, ADD_OPENS_KEY, i)) {
                final Entry manifestValueAndEndIdx = getManifestValue(manifest,
                        i + ADD_OPENS_KEY.length + 1);
                addExportsManifestEntryValue = manifestValueAndEndIdx.getKey();
                if (log != null) {
                    log.log("Found Add-Opens entry in manifest file: " + addOpensManifestEntryValue);
                }
                i = manifestValueAndEndIdx.getValue();

            } else if (keyMatchesAtPosition(manifest, AUTOMATIC_MODULE_NAME_KEY, i)) {
                final Entry manifestValueAndEndIdx = getManifestValue(manifest,
                        i + AUTOMATIC_MODULE_NAME_KEY.length + 1);
                automaticModuleNameManifestEntryValue = manifestValueAndEndIdx.getKey();
                if (log != null) {
                    log.log("Found Automatic-Module-Name entry in manifest file: "
                            + automaticModuleNameManifestEntryValue);
                }
                i = manifestValueAndEndIdx.getValue();

            } else {

                // Key name was unrecognized -- skip to next key
                skip = true;
            }

            if (skip) {
                // Field key didn't match -- skip to next key (after next newline that is not followed by a space)
                for (; i < manifest.length - 2; i++) {
                    if (manifest[i] == (byte) '\r' && manifest[i + 1] == (byte) '\n'
                            && manifest[i + 2] != (byte) ' ') {
                        i += 2;
                        break;
                    } else if ((manifest[i] == (byte) '\r' || manifest[i] == (byte) '\n')
                            && manifest[i + 1] != (byte) ' ') {
                        i++;
                        break;
                    }
                }
                if (i >= manifest.length - 2) {
                    break;
                }
            }
        }
    }

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

    /**
     * Read the central directory of the zipfile.
     * 
     * @param nestedJarHandler
     *            the nested jar handler
     * @param log
     *            the log
     * @throws IOException
     *             If an I/O exception occurs.
     * @throws InterruptedException
     *             if the thread was interrupted.
     */
    @SuppressWarnings("resource")
    private void readCentralDirectory(final NestedJarHandler nestedJarHandler, final LogNode log)
            throws IOException, InterruptedException {
        if (slice.sliceLength < 22) {
            throw new IOException("Zipfile too short to have a central directory");
        }

        final RandomAccessReader reader = slice.randomAccessReader();

        // Scan for End Of Central Directory (EOCD) signature. Final comment can be up to 64kB in length,
        // so need to scan back that far to determine if this is a valid zipfile. However for speed,
        // initially just try reading back a maximum of 32 characters.
        long eocdPos = -1;
        for (long i = slice.sliceLength - 22, iMin = slice.sliceLength - 22 - 32; i >= iMin && i >= 0L; --i) {
            if (reader.readUnsignedInt(i) == 0x06054b50L) {
                eocdPos = i;
                break;
            }
        }
        if (eocdPos < 0 && slice.sliceLength > 22 + 32) {
            // If EOCD signature was not found, read the last 64kB of file to RAM in a single chunk
            // so that we can scan back through it at higher speed to locate the EOCD signature
            final int bytesToRead = (int) Math.min(slice.sliceLength, 65536);
            final byte[] eocdBytes = new byte[bytesToRead];
            final long readStartOff = slice.sliceLength - bytesToRead;
            if (reader.read(readStartOff, eocdBytes, 0, bytesToRead) < bytesToRead) {
                // Should not happen
                throw new IOException("Zipfile is truncated");
            }
            try (final ArraySlice arraySlice = new ArraySlice(eocdBytes, /* isDeflatedZipEntry = */ false,
                    /* inflatedLengthHint = */ 0L, nestedJarHandler)) {
                final RandomAccessReader eocdReader = arraySlice.randomAccessReader();
                for (long i = eocdBytes.length - 22L; i >= 0L; --i) {
                    if (eocdReader.readUnsignedInt(i) == 0x06054b50L) {
                        eocdPos = i + readStartOff;
                        break;
                    }
                }
            }
        }
        if (eocdPos < 0) {
            throw new IOException("Jarfile central directory signature not found: " + getPath());
        }
        long numEnt = reader.readUnsignedShort(eocdPos + 8);
        if (reader.readUnsignedShort(eocdPos + 4) > 0 || reader.readUnsignedShort(eocdPos + 6) > 0
                || numEnt != reader.readUnsignedShort(eocdPos + 10)) {
            throw new IOException("Multi-disk jarfiles not supported: " + getPath());
        }
        long cenSize = reader.readUnsignedInt(eocdPos + 12);
        long cenOff = reader.readUnsignedInt(eocdPos + 16);
        long cenPos = eocdPos - cenSize;

        // Check for Zip64 End Of Central Directory Locator record
        final long zip64cdLocIdx = eocdPos - 20;
        if (zip64cdLocIdx >= 0 && reader.readUnsignedInt(zip64cdLocIdx) == 0x07064b50L) {
            if (reader.readUnsignedInt(zip64cdLocIdx + 4) > 0 || reader.readUnsignedInt(zip64cdLocIdx + 16) > 1) {
                throw new IOException("Multi-disk jarfiles not supported: " + getPath());
            }
            final long eocdPos64 = reader.readLong(zip64cdLocIdx + 8);
            if (reader.readUnsignedInt(eocdPos64) != 0x06064b50L) {
                throw new IOException("Zip64 central directory at location " + eocdPos64
                        + " does not have Zip64 central directory header: " + getPath());
            }
            final long numEnt64 = reader.readLong(eocdPos64 + 24);
            if (reader.readUnsignedInt(eocdPos64 + 16) > 0 || reader.readUnsignedInt(eocdPos64 + 20) > 0
                    || numEnt64 != reader.readLong(eocdPos64 + 32)) {
                throw new IOException("Multi-disk jarfiles not supported: " + getPath());
            }
            if (numEnt == 0xffff) {
                numEnt = numEnt64;
            } else if (numEnt != numEnt64) {
                // Entry size mismatch -- trigger manual counting of entries
                numEnt = -1L;
            }

            final long cenSize64 = reader.readLong(eocdPos64 + 40);
            if (cenSize == 0xffffffffL) {
                cenSize = cenSize64;
            } else if (cenSize != cenSize64) {
                throw new IOException(
                        "Mismatch in central directory size: " + cenSize + " vs. " + cenSize64 + ": " + getPath());
            }

            // Recalculate the central directory position
            cenPos = eocdPos64 - cenSize;

            final long cenOff64 = reader.readLong(eocdPos64 + 48);
            if (cenOff == 0xffffffffL) {
                cenOff = cenOff64;
            } else if (cenOff != cenOff64) {
                throw new IOException(
                        "Mismatch in central directory offset: " + cenOff + " vs. " + cenOff64 + ": " + getPath());
            }
        }

        if (cenSize > eocdPos) {
            throw new IOException(
                    "Central directory size out of range: " + cenSize + " vs. " + eocdPos + ": " + getPath());
        }

        // Get offset of first local file header
        final long locPos = cenPos - cenOff;
        if (locPos < 0) {
            throw new IOException("Local file header offset out of range: " + locPos + ": " + getPath());
        }

        // Read entries into a byte array, if central directory is smaller than 2GB. If central directory
        // is larger than 2GB, need to read each entry field from the file directly using ZipFileSliceReader.
        RandomAccessReader cenReader;
        if (cenSize > FileUtils.MAX_BUFFER_SIZE) {
            // Create a slice that covers the central directory (this allows a central directory larger than
            // 2GB to be accessed using the slower FileSlice API, which reads the file directly, but also
            // the slice can be accessed without adding cenPos to each read offset, so that this slice or
            // the slice in the "else" clause below are accessed with the same index, which is the offset
            // from the start of the central directory).
            cenReader = slice.slice(cenPos, cenSize, /* isDeflatedZipEntry = */ false, /* inflatedSizeHint = */ 0L)
                    .randomAccessReader();
        } else {
            // Read the central directory into RAM for speed, then wrap it in an ArraySlice
            // (random access is faster for ArraySlice than for FileSlice)
            final byte[] entryBytes = new byte[(int) cenSize];
            if (reader.read(cenPos, entryBytes, 0, (int) cenSize) < cenSize) {
                // Should not happen
                throw new IOException("Zipfile is truncated");
            }
            cenReader = new ArraySlice(entryBytes, /* isDeflatedZipEntry = */ false, /* inflatedSizeHint = */ 0L,
                    nestedJarHandler).randomAccessReader();
        }

        if (numEnt == -1L) {
            // numEnt and numEnt64 were inconsistent -- manually count entries
            numEnt = 0;
            for (long entOff = 0; entOff + 46 <= cenSize;) {
                final long sig = cenReader.readUnsignedInt(entOff);
                if (sig != 0x02014b50L) {
                    throw new IOException("Invalid central directory signature: 0x"
                            + Integer.toString((int) sig, 16) + ": " + getPath());
                }
                final int filenameLen = cenReader.readUnsignedShort(entOff + 28);
                final int extraFieldLen = cenReader.readUnsignedShort(entOff + 30);
                final int commentLen = cenReader.readUnsignedShort(entOff + 32);
                entOff += 46 + filenameLen + extraFieldLen + commentLen;
                numEnt++;
            }
        }

        //  Can't have more than (Integer.MAX_VALUE - 8) entries, since they are stored in an ArrayList
        if (numEnt > FileUtils.MAX_BUFFER_SIZE) {
            // One alternative in this (impossibly rare) situation would be to return only the first 2B entries
            throw new IOException("Too many zipfile entries: " + numEnt);
        }

        // Make sure there's no DoS attack vector by using a fake number of entries
        if (numEnt > cenSize / 46) {
            // The smallest directory entry is 46 bytes in size
            throw new IOException("Too many zipfile entries: " + numEnt + " (expected a max of " + cenSize / 46
                    + " based on central directory size)");
        }

        // Enumerate entries
        entries = new ArrayList<>((int) numEnt);
        FastZipEntry manifestZipEntry = null;
        try {
            int entSize = 0;
            for (long entOff = 0; entOff + 46 <= cenSize; entOff += entSize) {
                final long sig = cenReader.readUnsignedInt(entOff);
                if (sig != 0x02014b50L) {
                    throw new IOException("Invalid central directory signature: 0x"
                            + Integer.toString((int) sig, 16) + ": " + getPath());
                }
                final int filenameLen = cenReader.readUnsignedShort(entOff + 28);
                final int extraFieldLen = cenReader.readUnsignedShort(entOff + 30);
                final int commentLen = cenReader.readUnsignedShort(entOff + 32);
                entSize = 46 + filenameLen + extraFieldLen + commentLen;

                // Get and sanitize entry name
                final long filenameStartOff = entOff + 46;
                final long filenameEndOff = filenameStartOff + filenameLen;
                if (filenameEndOff > cenSize) {
                    if (log != null) {
                        log.log("Filename extends past end of entry -- skipping entry at offset " + entOff);
                    }
                    break;
                }
                final String entryName = cenReader.readString(filenameStartOff, filenameLen);
                String entryNameSanitized = FileUtils.sanitizeEntryPath(entryName, /* removeInitialSlash = */ true,
                        /* removeFinalSlash = */ false);
                if (entryNameSanitized.isEmpty() || entryName.endsWith("/")) {
                    // Skip directory entries
                    continue;
                }

                // Check entry flag bits
                final int flags = cenReader.readUnsignedShort(entOff + 8);
                if ((flags & 1) != 0) {
                    if (log != null) {
                        log.log("Skipping encrypted zip entry: " + entryNameSanitized);
                    }
                    continue;
                }

                // Check compression method
                final int compressionMethod = cenReader.readUnsignedShort(entOff + 10);
                if (compressionMethod != /* stored */ 0 && compressionMethod != /* deflated */ 8) {
                    if (log != null) {
                        log.log("Skipping zip entry with invalid compression method " + compressionMethod + ": "
                                + entryNameSanitized);
                    }
                    continue;
                }
                final boolean isDeflated = compressionMethod == /* deflated */ 8;

                // Get compressed and uncompressed size
                long compressedSize = (cenReader.readUnsignedInt(entOff + 20));
                long uncompressedSize = (cenReader.readUnsignedInt(entOff + 24));

                // Get external file attributes
                final int fileAttributes = cenReader.readUnsignedShort(entOff + 40);

                long pos = cenReader.readUnsignedInt(entOff + 42);

                // Check for Zip64 header in extra fields
                // See:
                // https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
                // https://github.com/LuaDist/zip/blob/master/proginfo/extrafld.txt
                long lastModifiedMillis = 0L;
                if (extraFieldLen > 0) {
                    for (int extraFieldOff = 0; extraFieldOff + 4 < extraFieldLen;) {
                        final long tagOff = filenameEndOff + extraFieldOff;
                        final int tag = cenReader.readUnsignedShort(tagOff);
                        final int size = cenReader.readUnsignedShort(tagOff + 2);
                        if (extraFieldOff + 4 + size > extraFieldLen) {
                            // Invalid size
                            if (log != null) {
                                log.log("Skipping zip entry with invalid extra field size: " + entryNameSanitized);
                            }
                            break;
                        }
                        if (tag == 1 && size >= 20) {
                            // Zip64 extended information extra field
                            final long uncompressedSize64 = cenReader.readLong(tagOff + 4 + 0);
                            if (uncompressedSize == 0xffffffffL) {
                                uncompressedSize = uncompressedSize64;
                            } else if (uncompressedSize != uncompressedSize64) {
                                throw new IOException("Mismatch in uncompressed size: " + uncompressedSize + " vs. "
                                        + uncompressedSize64 + ": " + entryNameSanitized);
                            }
                            final long compressedSize64 = cenReader.readLong(tagOff + 4 + 8);
                            if (compressedSize == 0xffffffffL) {
                                compressedSize = compressedSize64;
                            } else if (compressedSize != compressedSize64) {
                                throw new IOException("Mismatch in compressed size: " + compressedSize + " vs. "
                                        + compressedSize64 + ": " + entryNameSanitized);
                            }
                            // Only compressed size and uncompressed size are required fields
                            if (size >= 28) {
                                final long pos64 = cenReader.readLong(tagOff + 4 + 16);
                                if (pos == 0xffffffffL) {
                                    pos = pos64;
                                } else if (pos != pos64) {
                                    throw new IOException("Mismatch in entry pos: " + pos + " vs. " + pos64 + ": "
                                            + entryNameSanitized);
                                }
                            }
                            break;

                        } else if (tag == 0x5455 && size >= 5) {
                            // Extended Unix timestamp
                            final int bits = cenReader.readUnsignedByte(tagOff + 4 + 0);
                            if ((bits & 1) == 1 && size >= 5 + 8) {
                                lastModifiedMillis = cenReader.readLong(tagOff + 4 + 1) * 1000L;
                            }

                        } else if (tag == 0x5855 && size >= 20) {
                            // Unix extra field (deprecated)
                            lastModifiedMillis = cenReader.readLong(tagOff + 4 + 8) * 1000L;
                            // There are also optional UID and GID fields in this extra field (currently ignored)

                        } else if (tag == 0x7855) {
                            // Info-ZIP Unix UID and GID fields (currently ignored)

                        } else if (tag == 0x7075) {
                            // Info-ZIP Unicode path extra field
                            final int version = cenReader.readUnsignedByte(tagOff + 4 + 0);
                            if (version != 1) {
                                throw new IOException("Unknown Unicode entry name format " + version
                                        + " in extra field: " + entryNameSanitized);
                            } else if (size > 9) {
                                // Replace non-Unicode entry name with Unicode version
                                try {
                                    entryNameSanitized = cenReader.readString(tagOff + 9, size - 9);
                                } catch (final IllegalArgumentException e) {
                                    throw new IOException("Malformed extended Unicode entry name for entry: "
                                            + entryNameSanitized);
                                }
                            }
                        }
                        extraFieldOff += 4 + size;
                    }
                }

                int lastModifiedTimeMSDOS = 0;
                int lastModifiedDateMSDOS = 0;
                if (lastModifiedMillis == 0L) {
                    // If Unix timestamp was not provided, convert zip entry timestamp from MS-DOS format
                    lastModifiedTimeMSDOS = cenReader.readUnsignedShort(entOff + 12);
                    lastModifiedDateMSDOS = cenReader.readUnsignedShort(entOff + 14);
                }

                if (compressedSize < 0) {
                    if (log != null) {
                        log.log("Skipping zip entry with invalid compressed size (" + compressedSize + "): "
                                + entryNameSanitized);
                    }
                    continue;
                }
                if (uncompressedSize < 0) {
                    if (log != null) {
                        log.log("Skipping zip entry with invalid uncompressed size (" + uncompressedSize + "): "
                                + entryNameSanitized);
                    }
                    continue;
                }
                if (pos < 0) {
                    if (log != null) {
                        log.log("Skipping zip entry with invalid pos (" + pos + "): " + entryNameSanitized);
                    }
                    continue;
                }

                final long locHeaderPos = locPos + pos;
                if (locHeaderPos < 0) {
                    if (log != null) {
                        log.log("Skipping zip entry with invalid loc header position (" + locHeaderPos + "): "
                                + entryNameSanitized);
                    }
                    continue;
                }
                if (locHeaderPos + 4 >= slice.sliceLength) {
                    if (log != null) {
                        log.log("Unexpected EOF when trying to read LOC header: " + entryNameSanitized);
                    }
                    continue;
                }

                // Add zip entry
                final FastZipEntry entry = new FastZipEntry(this, locHeaderPos, entryNameSanitized, isDeflated,
                        compressedSize, uncompressedSize, lastModifiedMillis, lastModifiedTimeMSDOS,
                        lastModifiedDateMSDOS, fileAttributes, enableMultiReleaseVersions);
                entries.add(entry);

                // Record manifest entry
                if (entry.entryName.equals(MANIFEST_PATH)) {
                    manifestZipEntry = entry;
                }
            }
        } catch (EOFException | IndexOutOfBoundsException e) {
            // Stop reading entries if any entry is not within file
            if (log != null) {
                log.log("Reached premature EOF"
                        + (entries.isEmpty() ? "" : " after reading zip entry " + entries.get(entries.size() - 1)));
            }
        }

        // Parse manifest file, if present
        if (manifestZipEntry != null) {
            parseManifest(manifestZipEntry, log);
        }

        // For multi-release jars, drop any older or non-versioned entries that are masked by the most recent
        // version-specific entry
        if (isMultiReleaseJar) {
            if (VersionFinder.JAVA_MAJOR_VERSION < 9) {
                if (log != null) {
                    log.log("This is a multi-release jar, but JRE version " + VersionFinder.JAVA_MAJOR_VERSION
                            + " does not support multi-release jars");
                }
            } else {
                if (log != null) {
                    // Find all the unique multirelease versions within the jar
                    final Set versionsFound = new HashSet<>();
                    for (final FastZipEntry entry : entries) {
                        if (entry.version > 8) {
                            versionsFound.add(entry.version);
                        }
                    }
                    final List versionsFoundSorted = new ArrayList<>(versionsFound);
                    CollectionUtils.sortIfNotEmpty(versionsFoundSorted);
                    log.log("This is a multi-release jar, with versions: "
                            + StringUtils.join(", ", versionsFoundSorted));
                }

                // Sort in decreasing order of version in preparation for version masking
                CollectionUtils.sortIfNotEmpty(entries);

                // Mask files that appear in multiple version sections, so that there is only one entry
                // for each unversioned path, i.e. the versioned path with the highest version number
                final List unversionedZipEntriesMasked = new ArrayList<>(entries.size());
                final Map unversionedPathToVersionedPath = new HashMap<>();
                for (final FastZipEntry versionedZipEntry : entries) {
                    if (!unversionedPathToVersionedPath.containsKey(versionedZipEntry.entryNameUnversioned)) {
                        // This is the first FastZipEntry for this entry's unversioned path
                        unversionedPathToVersionedPath.put(versionedZipEntry.entryNameUnversioned,
                                versionedZipEntry.entryName);
                        unversionedZipEntriesMasked.add(versionedZipEntry);
                    } else if (log != null) {
                        log.log(unversionedPathToVersionedPath.get(versionedZipEntry.entryNameUnversioned)
                                + " masks " + versionedZipEntry.entryName);
                    }
                }

                // Override entries with version-masked entries
                entries = unversionedZipEntriesMasked;
            }
        }
    }

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

    @Override
    public boolean equals(final Object o) {
        return super.equals(o);
    }

    @Override
    public int hashCode() {
        return super.hashCode();
    }

    /* (non-Javadoc)
     * @see nonapi.io.github.classgraph.fastzipfilereader.ZipFileSlice#toString()
     */
    @Override
    public String toString() {
        return getPath();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy