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

cc.shacocloud.mirage.loader.jar.JarFileEntries Maven / Gradle / Ivy

There is a newer version: 2.1.0
Show newest version
package cc.shacocloud.mirage.loader.jar;

import cc.shacocloud.mirage.loader.data.RandomAccessData;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;

/**
 * 提供对来自 {@link JarFile} 的条目的访问。
 * 

* 为了减少内存消耗,使用数组存储条目详细信息。 * {@code hashCodes} 数组存储条目名称的哈希代码,{@code centralDirectoryOffsets} 提供中央目录记录的偏移量, * {@code position} 提供条目的原始顺序位置。数组按哈希码顺序存储,以便可以使用二叉搜索来查找名称。 *

* 一个典型的应用程序将有大约 10500 个条目,应该消耗大约 122K */ class JarFileEntries implements CentralDirectoryVisitor, Iterable { private static final Runnable NO_VALIDATION = () -> { }; private static final String META_INF_PREFIX = "META-INF/"; private static final Name MULTI_RELEASE = new Name("Multi-Release"); private static final int BASE_VERSION = 8; private static final int RUNTIME_VERSION; static { int version; try { Object runtimeVersion = Runtime.class.getMethod("version").invoke(null); version = (int) runtimeVersion.getClass().getMethod("major").invoke(runtimeVersion); } catch (Throwable ex) { version = BASE_VERSION; } RUNTIME_VERSION = version; } private static final long LOCAL_FILE_HEADER_SIZE = 30; private static final char SLASH = '/'; private static final char NO_SUFFIX = 0; protected static final int ENTRY_CACHE_SIZE = 25; private final JarFile jarFile; private final JarEntryFilter filter; private RandomAccessData centralDirectoryData; private int size; private int[] hashCodes; private Offsets centralDirectoryOffsets; private int[] positions; private Boolean multiReleaseJar; private JarEntryCertification[] certifications; private final Map entriesCache = Collections .synchronizedMap(new LinkedHashMap<>(16, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() >= ENTRY_CACHE_SIZE; } }); JarFileEntries(JarFile jarFile, JarEntryFilter filter) { this.jarFile = jarFile; this.filter = filter; if (RUNTIME_VERSION == BASE_VERSION) { this.multiReleaseJar = false; } } @Override public void visitStart(@NotNull CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) { int maxSize = endRecord.getNumberOfRecords(); this.centralDirectoryData = centralDirectoryData; this.hashCodes = new int[maxSize]; this.centralDirectoryOffsets = Offsets.from(endRecord); this.positions = new int[maxSize]; } @Override public void visitFileHeader(@NotNull CentralDirectoryFileHeader fileHeader, long dataOffset) { AsciiBytes name = applyFilter(fileHeader.getName()); if (name != null) { add(name, dataOffset); } } private void add(@NotNull AsciiBytes name, long dataOffset) { this.hashCodes[this.size] = name.hashCode(); this.centralDirectoryOffsets.set(this.size, dataOffset); this.positions[this.size] = this.size; this.size++; } @Override public void visitEnd() { sort(0, this.size - 1); int[] positions = this.positions; this.positions = new int[positions.length]; for (int i = 0; i < this.size; i++) { this.positions[positions[i]] = i; } } int getSize() { return this.size; } private void sort(int left, int right) { // 快速排序算法,使用哈希代码作为源,但对所有数组进行排序 if (left < right) { int pivot = this.hashCodes[left + (right - left) / 2]; int i = left; int j = right; while (i <= j) { while (this.hashCodes[i] < pivot) { i++; } while (this.hashCodes[j] > pivot) { j--; } if (i <= j) { swap(i, j); i++; j--; } } if (left < j) { sort(left, j); } if (right > i) { sort(i, right); } } } private void swap(int i, int j) { swap(this.hashCodes, i, j); this.centralDirectoryOffsets.swap(i, j); swap(this.positions, i, j); } @Override public Iterator iterator() { return new EntryIterator(NO_VALIDATION); } Iterator iterator(Runnable validator) { return new EntryIterator(validator); } boolean containsEntry(CharSequence name) { return getEntry(name, FileHeader.class, true) != null; } JarEntry getEntry(CharSequence name) { return getEntry(name, JarEntry.class, true); } InputStream getInputStream(String name) throws IOException { FileHeader entry = getEntry(name, FileHeader.class, false); return getInputStream(entry); } InputStream getInputStream(FileHeader entry) throws IOException { if (entry == null) { return null; } InputStream inputStream = getEntryData(entry).getInputStream(); if (entry.getMethod() == ZipEntry.DEFLATED) { inputStream = new ZipInflaterInputStream(inputStream, (int) entry.getSize()); } return inputStream; } RandomAccessData getEntryData(String name) throws IOException { FileHeader entry = getEntry(name, FileHeader.class, false); if (entry == null) { return null; } return getEntryData(entry); } private RandomAccessData getEntryData(@NotNull FileHeader entry) throws IOException { RandomAccessData data = this.jarFile.getData(); byte[] localHeader = data.read(entry.getLocalHeaderOffset(), LOCAL_FILE_HEADER_SIZE); long nameLength = Bytes.littleEndianValue(localHeader, 26, 2); long extraLength = Bytes.littleEndianValue(localHeader, 28, 2); return data.getSubsection(entry.getLocalHeaderOffset() + LOCAL_FILE_HEADER_SIZE + nameLength + extraLength, entry.getCompressedSize()); } private T getEntry(CharSequence name, Class type, boolean cacheEntry) { T entry = doGetEntry(name, type, cacheEntry, null); if (!isMetaInfEntry(name) && isMultiReleaseJar()) { int version = RUNTIME_VERSION; AsciiBytes nameAlias = (entry instanceof JarEntry) ? ((JarEntry) entry).getAsciiBytesName() : new AsciiBytes(name.toString()); while (version > BASE_VERSION) { T versionedEntry = doGetEntry("META-INF/versions/" + version + "/" + name, type, cacheEntry, nameAlias); if (versionedEntry != null) { return versionedEntry; } version--; } } return entry; } private boolean isMetaInfEntry(@NotNull CharSequence name) { return name.toString().startsWith(META_INF_PREFIX); } private boolean isMultiReleaseJar() { Boolean multiRelease = this.multiReleaseJar; if (multiRelease != null) { return multiRelease; } try { Manifest manifest = this.jarFile.getManifest(); if (manifest == null) { multiRelease = false; } else { Attributes attributes = manifest.getMainAttributes(); multiRelease = attributes.containsKey(MULTI_RELEASE); } } catch (IOException ex) { multiRelease = false; } this.multiReleaseJar = multiRelease; return multiRelease; } private T doGetEntry(CharSequence name, Class type, boolean cacheEntry, AsciiBytes nameAlias) { int hashCode = AsciiBytes.hashCode(name); T entry = getEntry(hashCode, name, NO_SUFFIX, type, cacheEntry, nameAlias); if (entry == null) { hashCode = AsciiBytes.hashCode(hashCode, SLASH); entry = getEntry(hashCode, name, SLASH, type, cacheEntry, nameAlias); } return entry; } private @Nullable T getEntry(int hashCode, CharSequence name, char suffix, Class type, boolean cacheEntry, AsciiBytes nameAlias) { int index = getFirstIndex(hashCode); while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) { T entry = getEntry(index, type, cacheEntry, nameAlias); if (entry.hasName(name, suffix)) { return entry; } index++; } return null; } @SuppressWarnings("unchecked") private @NotNull T getEntry(int index, Class type, boolean cacheEntry, AsciiBytes nameAlias) { try { long offset = this.centralDirectoryOffsets.get(index); FileHeader cached = this.entriesCache.get(index); FileHeader entry = (cached != null) ? cached : CentralDirectoryFileHeader.fromRandomAccessData(this.centralDirectoryData, offset, this.filter); if (CentralDirectoryFileHeader.class.equals(entry.getClass()) && type.equals(JarEntry.class)) { entry = new JarEntry(this.jarFile, index, (CentralDirectoryFileHeader) entry, nameAlias); } if (cacheEntry && cached != entry) { this.entriesCache.put(index, entry); } return (T) entry; } catch (IOException ex) { throw new IllegalStateException(ex); } } private int getFirstIndex(int hashCode) { int index = Arrays.binarySearch(this.hashCodes, 0, this.size, hashCode); if (index < 0) { return -1; } while (index > 0 && this.hashCodes[index - 1] == hashCode) { index--; } return index; } void clearCache() { this.entriesCache.clear(); } private AsciiBytes applyFilter(AsciiBytes name) { return (this.filter != null) ? this.filter.apply(name) : name; } JarEntryCertification getCertification(JarEntry entry) throws IOException { JarEntryCertification[] certifications = this.certifications; if (certifications == null) { certifications = new JarEntryCertification[this.size]; // 我们回退到 JarInputStream 来获取证书。这不是那么快,但希望不会经常发生 try (JarInputStream certifiedJarStream = new JarInputStream(this.jarFile.getData().getInputStream())) { java.util.jar.JarEntry certifiedEntry; while ((certifiedEntry = certifiedJarStream.getNextJarEntry()) != null) { // 必须关闭条目才能触发读取并设置条目证书 certifiedJarStream.closeEntry(); int index = getEntryIndex(certifiedEntry.getName()); if (index != -1) { certifications[index] = JarEntryCertification.from(certifiedEntry); } } } this.certifications = certifications; } JarEntryCertification certification = certifications[entry.getIndex()]; return (certification != null) ? certification : JarEntryCertification.NONE; } private int getEntryIndex(CharSequence name) { int hashCode = AsciiBytes.hashCode(name); int index = getFirstIndex(hashCode); while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) { FileHeader candidate = getEntry(index, FileHeader.class, false, null); if (candidate.hasName(name, NO_SUFFIX)) { return index; } index++; } return -1; } private static void swap(int @NotNull [] array, int i, int j) { int temp = array[i]; array[i] = array[j]; array[j] = temp; } private static void swap(long @NotNull [] array, int i, int j) { long temp = array[i]; array[i] = array[j]; array[j] = temp; } /** * 包含条目的迭代器 */ private final class EntryIterator implements Iterator { private final Runnable validator; private int index = 0; private EntryIterator(@NotNull Runnable validator) { this.validator = validator; validator.run(); } @Override public boolean hasNext() { this.validator.run(); return this.index < JarFileEntries.this.size; } @Override public @NotNull JarEntry next() { this.validator.run(); if (!hasNext()) { throw new NoSuchElementException(); } int entryIndex = JarFileEntries.this.positions[this.index]; this.index++; return getEntry(entryIndex, JarEntry.class, false, null); } } /** * 用于管理中央目录记录偏移的界面。常规 zip 文件由基于 {@code int[]} 的实现支持,Zip64 文件由 {@code long[]} 支持,并且会消耗更多内存。 */ private interface Offsets { void set(int index, long value); long get(int index); void swap(int i, int j); static @NotNull Offsets from(@NotNull CentralDirectoryEndRecord endRecord) { int size = endRecord.getNumberOfRecords(); return endRecord.isZip64() ? new Zip64Offsets(size) : new ZipOffsets(size); } } /** * 常规 zip 文件的 {@link Offsets} 实现 */ private static final class ZipOffsets implements Offsets { private final int[] offsets; private ZipOffsets(int size) { this.offsets = new int[size]; } @Override public void swap(int i, int j) { JarFileEntries.swap(this.offsets, i, j); } @Override public void set(int index, long value) { this.offsets[index] = (int) value; } @Override public long get(int index) { return this.offsets[index]; } } /** * zip64 文件的 {@link Offsets} 实现。 */ private static final class Zip64Offsets implements Offsets { private final long[] offsets; private Zip64Offsets(int size) { this.offsets = new long[size]; } @Override public void swap(int i, int j) { JarFileEntries.swap(this.offsets, i, j); } @Override public void set(int index, long value) { this.offsets[index] = value; } @Override public long get(int index) { return this.offsets[index]; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy