added library toc file

This commit is contained in:
2022-06-30 20:41:47 +08:00
parent 6f294d4dd7
commit 2b8e150949
34 changed files with 4249 additions and 59 deletions

View File

@@ -11,12 +11,6 @@ allprojects {
apply plugin: 'net.woggioni.gradle.lombok'
repositories {
maven {
url = woggioniMavenRepositoryUrl
content {
includeModule 'net.woggioni', 'xclassloader'
}
}
mavenCentral()
}

View File

@@ -15,6 +15,8 @@ public class Constants {
public static final String JAVA_AGENTS_FILE = METADATA_FOLDER + "/javaAgents.properties";
public static final String SYSTEM_PROPERTIES_FILE = METADATA_FOLDER + "/system.properties";
public static final String LIBRARIES_TOC = METADATA_FOLDER + "/libraries.txt";
public static class ManifestAttributes {
public static final String MAIN_MODULE = "Executable-Jar-Main-Module";
public static final String MAIN_CLASS = "Executable-Jar-Main-Class";

View File

@@ -6,6 +6,5 @@ lys-gradle-plugins.version = 2022.06
version.envelope=2022.06
version.gradle=7.4.2
version.lombok=1.18.22
version.xclassloader=1.0-SNAPSHOT
version.junitJupiter=5.7.2
version.junitPlatform=1.7.0

View File

@@ -22,7 +22,7 @@ configurations {
dependencies {
embedded project(path: ":common", configuration: 'archives')
embedded group: "net.woggioni", name: "xclassloader", version: getProperty("version.xclassloader")
embedded project(path: ":loader", configuration: 'archives')
}
java {

View File

@@ -1,26 +1,36 @@
package net.woggioni.envelope;
import lombok.SneakyThrows;
import net.woggioni.xclassloader.jar.JarFile;
import net.woggioni.envelope.loader.JarFile;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.function.Consumer;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.Manifest;
import static java.util.jar.JarFile.MANIFEST_NAME;
public class Launcher {
@SneakyThrows
static URL getURL(JarFile jarFile) {
return jarFile.getUrl();
}
@SneakyThrows
private static JarFile findCurrentJar() {
String launcherClassName = Launcher.class.getName();
@@ -86,7 +96,25 @@ public class Launcher {
String mainClassName = mainAttributes.getValue(Constants.ManifestAttributes.MAIN_CLASS);
String mainModuleName = mainAttributes.getValue(Constants.ManifestAttributes.MAIN_MODULE);
StringBuilder sb = new StringBuilder();
List<JarFile> classpath = new ArrayList<>();
URL libraryTocResource = Launcher.class.getClassLoader().getResource(Constants.LIBRARIES_TOC);
if(libraryTocResource == null) throw new RuntimeException(
Constants.LIBRARIES_TOC + " not found");
try(Reader reader = new InputStreamReader(libraryTocResource.openStream())) {
while(true) {
int c = reader.read();
boolean entryEnd = c == '/' || c < 0;
if(entryEnd) {
String entryName = Constants.LIBRARIES_FOLDER + '/' + sb;
JarEntry entry = currentJar.getJarEntry(entryName);
classpath.add(currentJar.getNestedJarFile(entry));
sb.setLength(0);
if(c < 0) break;
}
else sb.append((char) c);
}
}
Consumer<Class<?>> runner = new Consumer<Class<?>>() {
@Override
@SneakyThrows
@@ -108,7 +136,7 @@ public class Launcher {
currentJar,
mainModuleName,
mainClassName,
Constants.LIBRARIES_FOLDER,
classpath,
runner);
}

View File

@@ -1,33 +1,23 @@
package net.woggioni.envelope;
import lombok.SneakyThrows;
import net.woggioni.xclassloader.jar.JarFile;
import net.woggioni.envelope.loader.JarFile;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.stream.Collectors;
class MainRunner {
@SneakyThrows
static void run(JarFile currentJarFile,
String mainModuleName,
String mainClassName,
String librariesFolder,
Consumer<Class<?>> runner) {
List<URL> jarList = new ArrayList<>();
Enumeration<JarEntry> entries = currentJarFile.entries();
while(entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
if(!entry.isDirectory() && name.startsWith(librariesFolder) && name.endsWith(".jar")) {
jarList.add(currentJarFile.getNestedJarFile(entry).getUrl());
}
}
try (URLClassLoader cl = new URLClassLoader(jarList.toArray(new URL[0]), ClassLoader.getSystemClassLoader().getParent())) {
String mainModuleName,
String mainClassName,
List<JarFile> classpath,
Consumer<Class<?>> runner) {
URL[] urls = classpath.stream().map(Launcher::getURL).toArray(URL[]::new);
try (URLClassLoader cl = new URLClassLoader(urls, ClassLoader.getSystemClassLoader().getParent())) {
Thread.currentThread().setContextClassLoader(cl);
runner.accept(cl.loadClass(mainClassName));
}

View File

@@ -1,6 +1,6 @@
module net.woggioni.envelope {
requires java.logging;
requires static lombok;
requires net.woggioni.xclassloader;
requires net.woggioni.envelope.loader;
requires java.instrument;
}

View File

@@ -19,9 +19,9 @@ import java.net.URLClassLoader;
import lombok.SneakyThrows;
import net.woggioni.xclassloader.ModuleClassLoader;
import net.woggioni.xclassloader.JarFileModuleFinder;
import net.woggioni.xclassloader.jar.JarFile;
import net.woggioni.envelope.loader.ModuleClassLoader;
import net.woggioni.envelope.loader.JarFileModuleFinder;
import net.woggioni.envelope.loader.JarFile;
import java.util.jar.JarEntry;
class MainRunner {
@@ -34,35 +34,18 @@ class MainRunner {
static void run(JarFile currentJarFile,
String mainModuleName,
String mainClassName,
String librariesFolder,
List<JarFile> classpath,
Consumer<Class<?>> runner) {
if(mainModuleName == null) {
List<URL> jarList = new ArrayList<>();
Enumeration<JarEntry> entries = currentJarFile.entries();
while(entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
if(!entry.isDirectory() && name.startsWith(librariesFolder) && name.endsWith(".jar")) {
jarList.add(currentJarFile.getNestedJarFile(entry).getUrl());
}
}
try (URLClassLoader cl = new URLClassLoader(jarList.toArray(new URL[0]), ClassLoader.getSystemClassLoader().getParent())) {
URL[] urls = classpath.stream().map(Launcher::getURL).toArray(URL[]::new);
try (URLClassLoader cl = new URLClassLoader(urls, ClassLoader.getSystemClassLoader().getParent())) {
Thread.currentThread().setContextClassLoader(cl);
runner.accept(cl.loadClass(mainClassName));
}
} else {
List<JarFile> jarList = new ArrayList<>();
Enumeration<JarEntry> entries = currentJarFile.entries();
while(entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
if(!entry.isDirectory() && name.startsWith("LIB-INF") && name.endsWith(".jar")) {
jarList.add(currentJarFile.getNestedJarFile(entry));
}
}
ModuleLayer bootLayer = ModuleLayer.boot();
Configuration bootConfiguration = bootLayer.configuration();
JarFileModuleFinder jarFileModuleFinder = new JarFileModuleFinder(jarList);
JarFileModuleFinder jarFileModuleFinder = new JarFileModuleFinder(classpath);
Configuration cfg = bootConfiguration.resolve(jarFileModuleFinder, ModuleFinder.of(), Collections.singletonList(mainModuleName));
Map<String, ClassLoader> packageMap = new TreeMap<>();
ModuleLayer.Controller controller =

7
loader/build.gradle Normal file
View File

@@ -0,0 +1,7 @@
plugins {
id 'net.woggioni.gradle.multi-release-jar'
}
ext {
setProperty('jpms.module.name', 'net.woggioni.envelope.loader')
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.woggioni.envelope.loader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.Permission;
/**
* Base class for extended variants of {@link java.util.jar.JarFile}.
*
* @author Phillip Webb
*/
abstract class AbstractJarFile extends java.util.jar.JarFile {
/**
* Create a new {@link AbstractJarFile}.
* @param file the root jar file.
* @throws IOException on IO error
*/
AbstractJarFile(File file) throws IOException {
super(file);
}
/**
* Return a URL that can be used to access this JAR file. NOTE: the specified URL
* cannot be serialized and or cloned.
* @return the URL
* @throws MalformedURLException if the URL is malformed
*/
abstract URL getUrl() throws MalformedURLException;
/**
* Return the {@link JarFileType} of this instance.
* @return the jar file type
*/
abstract JarFileType getType();
/**
* Return the security permission for this JAR.
* @return the security permission.
*/
abstract Permission getPermission();
/**
* Return an {@link InputStream} for the entire jar contents.
* @return the contents input stream
* @throws IOException on IO error
*/
abstract InputStream getInputStream() throws IOException;
/**
* The type of a {@link JarFile}.
*/
enum JarFileType {
DIRECT, NESTED_DIRECTORY, NESTED_JAR
}
}

View File

@@ -0,0 +1,415 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.woggioni.envelope.loader;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
* Utilities for dealing with bytes from ZIP files.
*
* @author Phillip Webb
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
final class Bytes {
static long littleEndianValue(byte[] bytes, int offset, int length) {
long value = 0;
for (int i = length - 1; i >= 0; i--) {
value = ((value << 8) | (bytes[offset + i] & 0xFF));
}
return value;
}
/**
* Simple wrapper around a byte array that represents an ASCII. Used for performance
* reasons to save constructing Strings for ZIP data.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
static final class AsciiBytes {
private static final String EMPTY_STRING = "";
private static final int[] INITIAL_BYTE_BITMASK = { 0x7F, 0x1F, 0x0F, 0x07 };
private static final int SUBSEQUENT_BYTE_BITMASK = 0x3F;
private final byte[] bytes;
private final int offset;
private final int length;
private String string;
private int hash;
/**
* Create a new {@link AsciiBytes} from the specified String.
* @param string the source string
*/
AsciiBytes(String string) {
this(string.getBytes(StandardCharsets.UTF_8));
this.string = string;
}
/**
* Create a new {@link AsciiBytes} from the specified bytes. NOTE: underlying bytes
* are not expected to change.
* @param bytes the source bytes
*/
AsciiBytes(byte[] bytes) {
this(bytes, 0, bytes.length);
}
/**
* Create a new {@link AsciiBytes} from the specified bytes. NOTE: underlying bytes
* are not expected to change.
* @param bytes the source bytes
* @param offset the offset
* @param length the length
*/
AsciiBytes(byte[] bytes, int offset, int length) {
if (offset < 0 || length < 0 || (offset + length) > bytes.length) {
throw new IndexOutOfBoundsException();
}
this.bytes = bytes;
this.offset = offset;
this.length = length;
}
int length() {
return this.length;
}
boolean startsWith(AsciiBytes prefix) {
if (this == prefix) {
return true;
}
if (prefix.length > this.length) {
return false;
}
for (int i = 0; i < prefix.length; i++) {
if (this.bytes[i + this.offset] != prefix.bytes[i + prefix.offset]) {
return false;
}
}
return true;
}
boolean endsWith(AsciiBytes postfix) {
if (this == postfix) {
return true;
}
if (postfix.length > this.length) {
return false;
}
for (int i = 0; i < postfix.length; i++) {
if (this.bytes[this.offset + (this.length - 1) - i] != postfix.bytes[postfix.offset + (postfix.length - 1)
- i]) {
return false;
}
}
return true;
}
AsciiBytes substring(int beginIndex) {
return substring(beginIndex, this.length);
}
AsciiBytes substring(int beginIndex, int endIndex) {
int length = endIndex - beginIndex;
if (this.offset + length > this.bytes.length) {
throw new IndexOutOfBoundsException();
}
return new AsciiBytes(this.bytes, this.offset + beginIndex, length);
}
boolean matches(CharSequence name, char suffix) {
int charIndex = 0;
int nameLen = name.length();
int totalLen = nameLen + ((suffix != 0) ? 1 : 0);
for (int i = this.offset; i < this.offset + this.length; i++) {
int b = this.bytes[i];
int remainingUtfBytes = getNumberOfUtfBytes(b) - 1;
b &= INITIAL_BYTE_BITMASK[remainingUtfBytes];
for (int j = 0; j < remainingUtfBytes; j++) {
b = (b << 6) + (this.bytes[++i] & SUBSEQUENT_BYTE_BITMASK);
}
char c = getChar(name, suffix, charIndex++);
if (b <= 0xFFFF) {
if (c != b) {
return false;
}
}
else {
if (c != ((b >> 0xA) + 0xD7C0)) {
return false;
}
c = getChar(name, suffix, charIndex++);
if (c != ((b & 0x3FF) + 0xDC00)) {
return false;
}
}
}
return charIndex == totalLen;
}
private char getChar(CharSequence name, char suffix, int index) {
if (index < name.length()) {
return name.charAt(index);
}
if (index == name.length()) {
return suffix;
}
return 0;
}
private int getNumberOfUtfBytes(int b) {
if ((b & 0x80) == 0) {
return 1;
}
int numberOfUtfBytes = 0;
while ((b & 0x80) != 0) {
b <<= 1;
numberOfUtfBytes++;
}
return numberOfUtfBytes;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (obj.getClass() == AsciiBytes.class) {
AsciiBytes other = (AsciiBytes) obj;
if (this.length == other.length) {
for (int i = 0; i < this.length; i++) {
if (this.bytes[this.offset + i] != other.bytes[other.offset + i]) {
return false;
}
}
return true;
}
}
return false;
}
@Override
public int hashCode() {
int hash = this.hash;
if (hash == 0 && this.bytes.length > 0) {
for (int i = this.offset; i < this.offset + this.length; i++) {
int b = this.bytes[i];
int remainingUtfBytes = getNumberOfUtfBytes(b) - 1;
b &= INITIAL_BYTE_BITMASK[remainingUtfBytes];
for (int j = 0; j < remainingUtfBytes; j++) {
b = (b << 6) + (this.bytes[++i] & SUBSEQUENT_BYTE_BITMASK);
}
if (b <= 0xFFFF) {
hash = 31 * hash + b;
}
else {
hash = 31 * hash + ((b >> 0xA) + 0xD7C0);
hash = 31 * hash + ((b & 0x3FF) + 0xDC00);
}
}
this.hash = hash;
}
return hash;
}
@Override
public String toString() {
if (this.string == null) {
if (this.length == 0) {
this.string = EMPTY_STRING;
}
else {
this.string = new String(this.bytes, this.offset, this.length, StandardCharsets.UTF_8);
}
}
return this.string;
}
static String toString(byte[] bytes) {
return new String(bytes, StandardCharsets.UTF_8);
}
static int hashCode(CharSequence charSequence) {
// We're compatible with String's hashCode()
if (charSequence instanceof StringSequence) {
// ... but save making an unnecessary String for StringSequence
return charSequence.hashCode();
}
return charSequence.toString().hashCode();
}
static int hashCode(int hash, char suffix) {
return (suffix != 0) ? (31 * hash + suffix) : hash;
}
}
/**
* A {@link CharSequence} backed by a single shared {@link String}. Unlike a regular
* {@link String}, {@link #subSequence(int, int)} operations will not copy the underlying
* character array.
*
* @author Phillip Webb
*/
static final class StringSequence implements CharSequence {
private final String source;
private final int start;
private final int end;
private int hash;
StringSequence(String source) {
this(source, 0, (source != null) ? source.length() : -1);
}
StringSequence(String source, int start, int end) {
Objects.requireNonNull(source, "Source must not be null");
if (start < 0) {
throw new StringIndexOutOfBoundsException(start);
}
if (end > source.length()) {
throw new StringIndexOutOfBoundsException(end);
}
this.source = source;
this.start = start;
this.end = end;
}
StringSequence subSequence(int start) {
return subSequence(start, length());
}
@Override
public StringSequence subSequence(int start, int end) {
int subSequenceStart = this.start + start;
int subSequenceEnd = this.start + end;
if (subSequenceStart > this.end) {
throw new StringIndexOutOfBoundsException(start);
}
if (subSequenceEnd > this.end) {
throw new StringIndexOutOfBoundsException(end);
}
if (start == 0 && subSequenceEnd == this.end) {
return this;
}
return new StringSequence(this.source, subSequenceStart, subSequenceEnd);
}
/**
* Returns {@code true} if the sequence is empty. Public to be compatible with JDK 15.
* @return {@code true} if {@link #length()} is {@code 0}, otherwise {@code false}
*/
public boolean isEmpty() {
return length() == 0;
}
@Override
public int length() {
return this.end - this.start;
}
@Override
public char charAt(int index) {
return this.source.charAt(this.start + index);
}
int indexOf(char ch) {
return this.source.indexOf(ch, this.start) - this.start;
}
int indexOf(String str) {
return this.source.indexOf(str, this.start) - this.start;
}
int indexOf(String str, int fromIndex) {
return this.source.indexOf(str, this.start + fromIndex) - this.start;
}
boolean startsWith(String prefix) {
return startsWith(prefix, 0);
}
boolean startsWith(String prefix, int offset) {
int prefixLength = prefix.length();
int length = length();
if (length - prefixLength - offset < 0) {
return false;
}
return this.source.startsWith(prefix, this.start + offset);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof CharSequence)) {
return false;
}
CharSequence other = (CharSequence) obj;
int n = length();
if (n != other.length()) {
return false;
}
int i = 0;
while (n-- != 0) {
if (charAt(i) != other.charAt(i)) {
return false;
}
i++;
}
return true;
}
@Override
public int hashCode() {
int hash = this.hash;
if (hash == 0 && length() > 0) {
for (int i = this.start; i < this.end; i++) {
hash = 31 * hash + this.source.charAt(i);
}
this.hash = hash;
}
return hash;
}
@Override
public String toString() {
return this.source.substring(this.start, this.end);
}
}
}

View File

@@ -0,0 +1,256 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.woggioni.envelope.loader;
import java.io.IOException;
/**
* A ZIP File "End of central directory record" (EOCD).
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Camille Vienot
* @see <a href="https://en.wikipedia.org/wiki/Zip_%28file_format%29">Zip File Format</a>
*/
class CentralDirectoryEndRecord {
private static final int MINIMUM_SIZE = 22;
private static final int MAXIMUM_COMMENT_LENGTH = 0xFFFF;
private static final int MAXIMUM_SIZE = MINIMUM_SIZE + MAXIMUM_COMMENT_LENGTH;
private static final int SIGNATURE = 0x06054b50;
private static final int COMMENT_LENGTH_OFFSET = 20;
private static final int READ_BLOCK_SIZE = 256;
private final Zip64End zip64End;
private byte[] block;
private int offset;
private int size;
/**
* Create a new {@link CentralDirectoryEndRecord} instance from the specified
* {@link RandomAccessData}, searching backwards from the end until a valid block is
* located.
* @param data the source data
* @throws IOException in case of I/O errors
*/
CentralDirectoryEndRecord(RandomAccessData data) throws IOException {
this.block = createBlockFromEndOfData(data, READ_BLOCK_SIZE);
this.size = MINIMUM_SIZE;
this.offset = this.block.length - this.size;
while (!isValid()) {
this.size++;
if (this.size > this.block.length) {
if (this.size >= MAXIMUM_SIZE || this.size > data.getSize()) {
throw new IOException(
"Unable to find ZIP central directory records after reading " + this.size + " bytes");
}
this.block = createBlockFromEndOfData(data, this.size + READ_BLOCK_SIZE);
}
this.offset = this.block.length - this.size;
}
long startOfCentralDirectoryEndRecord = data.getSize() - this.size;
Zip64Locator zip64Locator = Zip64Locator.find(data, startOfCentralDirectoryEndRecord);
this.zip64End = (zip64Locator != null) ? new Zip64End(data, zip64Locator) : null;
}
private byte[] createBlockFromEndOfData(RandomAccessData data, int size) throws IOException {
int length = (int) Math.min(data.getSize(), size);
return data.read(data.getSize() - length, length);
}
private boolean isValid() {
if (this.block.length < MINIMUM_SIZE || Bytes.littleEndianValue(this.block, this.offset + 0, 4) != SIGNATURE) {
return false;
}
// Total size must be the structure size + comment
long commentLength = Bytes.littleEndianValue(this.block, this.offset + COMMENT_LENGTH_OFFSET, 2);
return this.size == MINIMUM_SIZE + commentLength;
}
/**
* Returns the location in the data that the archive actually starts. For most files
* the archive data will start at 0, however, it is possible to have prefixed bytes
* (often used for startup scripts) at the beginning of the data.
* @param data the source data
* @return the offset within the data where the archive begins
*/
long getStartOfArchive(RandomAccessData data) {
long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4);
long specifiedOffset = (this.zip64End != null) ? this.zip64End.centralDirectoryOffset
: Bytes.littleEndianValue(this.block, this.offset + 16, 4);
long zip64EndSize = (this.zip64End != null) ? this.zip64End.getSize() : 0L;
int zip64LocSize = (this.zip64End != null) ? Zip64Locator.ZIP64_LOCSIZE : 0;
long actualOffset = data.getSize() - this.size - length - zip64EndSize - zip64LocSize;
return actualOffset - specifiedOffset;
}
/**
* Return the bytes of the "Central directory" based on the offset indicated in this
* record.
* @param data the source data
* @return the central directory data
*/
RandomAccessData getCentralDirectory(RandomAccessData data) {
if (this.zip64End != null) {
return this.zip64End.getCentralDirectory(data);
}
long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4);
long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4);
return data.getSubsection(offset, length);
}
/**
* Return the number of ZIP entries in the file.
* @return the number of records in the zip
*/
int getNumberOfRecords() {
if (this.zip64End != null) {
return this.zip64End.getNumberOfRecords();
}
long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10, 2);
return (int) numberOfRecords;
}
String getComment() {
int commentLength = (int) Bytes.littleEndianValue(this.block, this.offset + COMMENT_LENGTH_OFFSET, 2);
Bytes.AsciiBytes comment = new Bytes.AsciiBytes(this.block, this.offset + COMMENT_LENGTH_OFFSET + 2, commentLength);
return comment.toString();
}
boolean isZip64() {
return this.zip64End != null;
}
/**
* A Zip64 end of central directory record.
*
* @see <a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT">Chapter
* 4.3.14 of Zip64 specification</a>
*/
private static final class Zip64End {
private static final int ZIP64_ENDTOT = 32; // total number of entries
private static final int ZIP64_ENDSIZ = 40; // central directory size in bytes
private static final int ZIP64_ENDOFF = 48; // offset of first CEN header
private final Zip64Locator locator;
private final long centralDirectoryOffset;
private final long centralDirectoryLength;
private final int numberOfRecords;
private Zip64End(RandomAccessData data, Zip64Locator locator) throws IOException {
this.locator = locator;
byte[] block = data.read(locator.getZip64EndOffset(), 56);
this.centralDirectoryOffset = Bytes.littleEndianValue(block, ZIP64_ENDOFF, 8);
this.centralDirectoryLength = Bytes.littleEndianValue(block, ZIP64_ENDSIZ, 8);
this.numberOfRecords = (int) Bytes.littleEndianValue(block, ZIP64_ENDTOT, 8);
}
/**
* Return the size of this zip 64 end of central directory record.
* @return size of this zip 64 end of central directory record
*/
private long getSize() {
return this.locator.getZip64EndSize();
}
/**
* Return the bytes of the "Central directory" based on the offset indicated in
* this record.
* @param data the source data
* @return the central directory data
*/
private RandomAccessData getCentralDirectory(RandomAccessData data) {
return data.getSubsection(this.centralDirectoryOffset, this.centralDirectoryLength);
}
/**
* Return the number of entries in the zip64 archive.
* @return the number of records in the zip
*/
private int getNumberOfRecords() {
return this.numberOfRecords;
}
}
/**
* A Zip64 end of central directory locator.
*
* @see <a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT">Chapter
* 4.3.15 of Zip64 specification</a>
*/
private static final class Zip64Locator {
static final int SIGNATURE = 0x07064b50;
static final int ZIP64_LOCSIZE = 20; // locator size
static final int ZIP64_LOCOFF = 8; // offset of zip64 end
private final long zip64EndOffset;
private final long offset;
private Zip64Locator(long offset, byte[] block) {
this.offset = offset;
this.zip64EndOffset = Bytes.littleEndianValue(block, ZIP64_LOCOFF, 8);
}
/**
* Return the size of the zip 64 end record located by this zip64 end locator.
* @return size of the zip 64 end record located by this zip64 end locator
*/
private long getZip64EndSize() {
return this.offset - this.zip64EndOffset;
}
/**
* Return the offset to locate {@link Zip64End}.
* @return offset of the Zip64 end of central directory record
*/
private long getZip64EndOffset() {
return this.zip64EndOffset;
}
private static Zip64Locator find(RandomAccessData data, long centralDirectoryEndOffset) throws IOException {
long offset = centralDirectoryEndOffset - ZIP64_LOCSIZE;
if (offset >= 0) {
byte[] block = data.read(offset, ZIP64_LOCSIZE);
if (Bytes.littleEndianValue(block, 0, 4) == SIGNATURE) {
return new Zip64Locator(offset, block);
}
}
return null;
}
}
}

View File

@@ -0,0 +1,218 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.woggioni.envelope.loader;
import java.io.IOException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.ValueRange;
/**
* A ZIP File "Central directory file header record" (CDFH).
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Dmytro Nosan
* @see <a href="https://en.wikipedia.org/wiki/Zip_%28file_format%29">Zip File Format</a>
*/
final class CentralDirectoryFileHeader implements FileHeader {
private static final Bytes.AsciiBytes SLASH = new Bytes.AsciiBytes("/");
private static final byte[] NO_EXTRA = {};
private static final Bytes.AsciiBytes NO_COMMENT = new Bytes.AsciiBytes("");
private byte[] header;
private int headerOffset;
private Bytes.AsciiBytes name;
private byte[] extra;
private Bytes.AsciiBytes comment;
private long localHeaderOffset;
CentralDirectoryFileHeader() {
}
CentralDirectoryFileHeader(byte[] header, int headerOffset, Bytes.AsciiBytes name, byte[] extra, Bytes.AsciiBytes comment,
long localHeaderOffset) {
this.header = header;
this.headerOffset = headerOffset;
this.name = name;
this.extra = extra;
this.comment = comment;
this.localHeaderOffset = localHeaderOffset;
}
void load(byte[] data, int dataOffset, RandomAccessData variableData, long variableOffset, JarEntryFilter filter)
throws IOException {
// Load fixed part
this.header = data;
this.headerOffset = dataOffset;
long compressedSize = Bytes.littleEndianValue(data, dataOffset + 20, 4);
long uncompressedSize = Bytes.littleEndianValue(data, dataOffset + 24, 4);
long nameLength = Bytes.littleEndianValue(data, dataOffset + 28, 2);
long extraLength = Bytes.littleEndianValue(data, dataOffset + 30, 2);
long commentLength = Bytes.littleEndianValue(data, dataOffset + 32, 2);
long localHeaderOffset = Bytes.littleEndianValue(data, dataOffset + 42, 4);
// Load variable part
dataOffset += 46;
if (variableData != null) {
data = variableData.read(variableOffset + 46, nameLength + extraLength + commentLength);
dataOffset = 0;
}
this.name = new Bytes.AsciiBytes(data, dataOffset, (int) nameLength);
if (filter != null) {
this.name = filter.apply(this.name);
}
this.extra = NO_EXTRA;
this.comment = NO_COMMENT;
if (extraLength > 0) {
this.extra = new byte[(int) extraLength];
System.arraycopy(data, (int) (dataOffset + nameLength), this.extra, 0, this.extra.length);
}
this.localHeaderOffset = getLocalHeaderOffset(compressedSize, uncompressedSize, localHeaderOffset, this.extra);
if (commentLength > 0) {
this.comment = new Bytes.AsciiBytes(data, (int) (dataOffset + nameLength + extraLength), (int) commentLength);
}
}
private long getLocalHeaderOffset(long compressedSize, long uncompressedSize, long localHeaderOffset, byte[] extra)
throws IOException {
if (localHeaderOffset != 0xFFFFFFFFL) {
return localHeaderOffset;
}
int extraOffset = 0;
while (extraOffset < extra.length - 2) {
int id = (int) Bytes.littleEndianValue(extra, extraOffset, 2);
int length = (int) Bytes.littleEndianValue(extra, extraOffset, 2);
extraOffset += 4;
if (id == 1) {
int localHeaderExtraOffset = 0;
if (compressedSize == 0xFFFFFFFFL) {
localHeaderExtraOffset += 4;
}
if (uncompressedSize == 0xFFFFFFFFL) {
localHeaderExtraOffset += 4;
}
return Bytes.littleEndianValue(extra, extraOffset + localHeaderExtraOffset, 8);
}
extraOffset += length;
}
throw new IOException("Zip64 Extended Information Extra Field not found");
}
Bytes.AsciiBytes getName() {
return this.name;
}
@Override
public boolean hasName(CharSequence name, char suffix) {
return this.name.matches(name, suffix);
}
boolean isDirectory() {
return this.name.endsWith(SLASH);
}
@Override
public int getMethod() {
return (int) Bytes.littleEndianValue(this.header, this.headerOffset + 10, 2);
}
long getTime() {
long datetime = Bytes.littleEndianValue(this.header, this.headerOffset + 12, 4);
return decodeMsDosFormatDateTime(datetime);
}
/**
* Decode MS-DOS Date Time details. See <a href=
* "https://docs.microsoft.com/en-gb/windows/desktop/api/winbase/nf-winbase-dosdatetimetofiletime">
* Microsoft's documentation</a> for more details of the format.
* @param datetime the date and time
* @return the date and time as milliseconds since the epoch
*/
private long decodeMsDosFormatDateTime(long datetime) {
int year = getChronoValue(((datetime >> 25) & 0x7f) + 1980, ChronoField.YEAR);
int month = getChronoValue((datetime >> 21) & 0x0f, ChronoField.MONTH_OF_YEAR);
int day = getChronoValue((datetime >> 16) & 0x1f, ChronoField.DAY_OF_MONTH);
int hour = getChronoValue((datetime >> 11) & 0x1f, ChronoField.HOUR_OF_DAY);
int minute = getChronoValue((datetime >> 5) & 0x3f, ChronoField.MINUTE_OF_HOUR);
int second = getChronoValue((datetime << 1) & 0x3e, ChronoField.SECOND_OF_MINUTE);
return ZonedDateTime.of(year, month, day, hour, minute, second, 0, ZoneId.systemDefault()).toInstant()
.truncatedTo(ChronoUnit.SECONDS).toEpochMilli();
}
long getCrc() {
return Bytes.littleEndianValue(this.header, this.headerOffset + 16, 4);
}
@Override
public long getCompressedSize() {
return Bytes.littleEndianValue(this.header, this.headerOffset + 20, 4);
}
@Override
public long getSize() {
return Bytes.littleEndianValue(this.header, this.headerOffset + 24, 4);
}
byte[] getExtra() {
return this.extra;
}
boolean hasExtra() {
return this.extra.length > 0;
}
Bytes.AsciiBytes getComment() {
return this.comment;
}
@Override
public long getLocalHeaderOffset() {
return this.localHeaderOffset;
}
@Override
public CentralDirectoryFileHeader clone() {
byte[] header = new byte[46];
System.arraycopy(this.header, this.headerOffset, header, 0, header.length);
return new CentralDirectoryFileHeader(header, 0, this.name, header, this.comment, this.localHeaderOffset);
}
static CentralDirectoryFileHeader fromRandomAccessData(RandomAccessData data, long offset, JarEntryFilter filter)
throws IOException {
CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader();
byte[] bytes = data.read(offset, 46);
fileHeader.load(bytes, 0, data, offset, filter);
return fileHeader;
}
private static int getChronoValue(long value, ChronoField field) {
ValueRange range = field.range();
return Math.toIntExact(Math.min(Math.max(value, range.getMinimum()), range.getMaximum()));
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.woggioni.envelope.loader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Parses the central directory from a JAR file.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @see CentralDirectoryVisitor
*/
class CentralDirectoryParser {
private static final int CENTRAL_DIRECTORY_HEADER_BASE_SIZE = 46;
private final List<CentralDirectoryVisitor> visitors = new ArrayList<>();
<T extends CentralDirectoryVisitor> T addVisitor(T visitor) {
this.visitors.add(visitor);
return visitor;
}
/**
* Parse the source data, triggering {@link CentralDirectoryVisitor visitors}.
* @param data the source data
* @param skipPrefixBytes if prefix bytes should be skipped
* @return the actual archive data without any prefix bytes
* @throws IOException on error
*/
RandomAccessData parse(RandomAccessData data, boolean skipPrefixBytes) throws IOException {
CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord(data);
if (skipPrefixBytes) {
data = getArchiveData(endRecord, data);
}
RandomAccessData centralDirectoryData = endRecord.getCentralDirectory(data);
visitStart(endRecord, centralDirectoryData);
parseEntries(endRecord, centralDirectoryData);
visitEnd();
return data;
}
private void parseEntries(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData)
throws IOException {
byte[] bytes = centralDirectoryData.read(0, centralDirectoryData.getSize());
CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader();
int dataOffset = 0;
for (int i = 0; i < endRecord.getNumberOfRecords(); i++) {
fileHeader.load(bytes, dataOffset, null, 0, null);
visitFileHeader(dataOffset, fileHeader);
dataOffset += CENTRAL_DIRECTORY_HEADER_BASE_SIZE + fileHeader.getName().length()
+ fileHeader.getComment().length() + fileHeader.getExtra().length;
}
}
private RandomAccessData getArchiveData(CentralDirectoryEndRecord endRecord, RandomAccessData data) {
long offset = endRecord.getStartOfArchive(data);
if (offset == 0) {
return data;
}
return data.getSubsection(offset, data.getSize() - offset);
}
private void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) {
for (CentralDirectoryVisitor visitor : this.visitors) {
visitor.visitStart(endRecord, centralDirectoryData);
}
}
private void visitFileHeader(long dataOffset, CentralDirectoryFileHeader fileHeader) {
for (CentralDirectoryVisitor visitor : this.visitors) {
visitor.visitFileHeader(fileHeader, dataOffset);
}
}
private void visitEnd() {
for (CentralDirectoryVisitor visitor : this.visitors) {
visitor.visitEnd();
}
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.woggioni.envelope.loader;
/**
* Callback visitor triggered by {@link CentralDirectoryParser}.
*
* @author Phillip Webb
*/
interface CentralDirectoryVisitor {
void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData);
void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset);
void visitEnd();
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.woggioni.envelope.loader;
import java.util.zip.ZipEntry;
/**
* A file header record that has been loaded from a Jar file.
*
* @author Phillip Webb
* @see JarEntry
* @see CentralDirectoryFileHeader
*/
interface FileHeader {
/**
* Returns {@code true} if the header has the given name.
* @param name the name to test
* @param suffix an additional suffix (or {@code 0})
* @return {@code true} if the header has the given name
*/
boolean hasName(CharSequence name, char suffix);
/**
* Return the offset of the load file header within the archive data.
* @return the local header offset
*/
long getLocalHeaderOffset();
/**
* Return the compressed size of the entry.
* @return the compressed size.
*/
long getCompressedSize();
/**
* Return the uncompressed size of the entry.
* @return the uncompressed size.
*/
long getSize();
/**
* Return the method used to compress the data.
* @return the zip compression method
* @see ZipEntry#STORED
* @see ZipEntry#DEFLATED
*/
int getMethod();
}

View File

@@ -0,0 +1,466 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.woggioni.envelope.loader;
import java.io.File;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
/**
* {@link URLStreamHandler} for Spring Boot loader {@link JarFile}s.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.0.0
* @see JarFile#registerUrlProtocolHandler()
*/
public class Handler extends URLStreamHandler {
// NOTE: in order to be found as a URL protocol handler, this class must be public,
// must be named Handler and must be in a package ending '.jar'
private static final String JAR_PROTOCOL = "jar:";
private static final String FILE_PROTOCOL = "file:";
private static final String TOMCAT_WARFILE_PROTOCOL = "war:file:";
private static final String SEPARATOR = "!/";
private static final Pattern SEPARATOR_PATTERN = Pattern.compile(SEPARATOR, Pattern.LITERAL);
private static final String CURRENT_DIR = "/./";
private static final Pattern CURRENT_DIR_PATTERN = Pattern.compile(CURRENT_DIR, Pattern.LITERAL);
private static final String PARENT_DIR = "/../";
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
private static final String[] FALLBACK_HANDLERS = { "sun.net.www.protocol.jar.Handler" };
private static URL jarContextUrl;
private static SoftReference<Map<File, JarFile>> rootFileCache;
static {
rootFileCache = new SoftReference<>(null);
}
private final JarFile jarFile;
private URLStreamHandler fallbackHandler;
public Handler() {
this(null);
}
public Handler(JarFile jarFile) {
this.jarFile = jarFile;
}
@Override
protected URLConnection openConnection(URL url) throws IOException {
if (this.jarFile != null && isUrlInJarFile(url, this.jarFile)) {
return JarURLConnection.get(url, this.jarFile);
}
try {
return JarURLConnection.get(url, getRootJarFileFromUrl(url));
}
catch (Exception ex) {
return openFallbackConnection(url, ex);
}
}
private boolean isUrlInJarFile(URL url, JarFile jarFile) throws MalformedURLException {
// Try the path first to save building a new url string each time
return url.getPath().startsWith(jarFile.getUrl().getPath())
&& url.toString().startsWith(jarFile.getUrlString());
}
private URLConnection openFallbackConnection(URL url, Exception reason) throws IOException {
try {
URLConnection connection = openFallbackTomcatConnection(url);
connection = (connection != null) ? connection : openFallbackContextConnection(url);
return (connection != null) ? connection : openFallbackHandlerConnection(url);
}
catch (Exception ex) {
if (reason instanceof IOException) {
log(false, "Unable to open fallback handler", ex);
throw (IOException) reason;
}
log(true, "Unable to open fallback handler", ex);
if (reason instanceof RuntimeException) {
throw (RuntimeException) reason;
}
throw new IllegalStateException(reason);
}
}
/**
* Attempt to open a Tomcat formatted 'jar:war:file:...' URL. This method allows us to
* use our own nested JAR support to open the content rather than the logic in
* {@code sun.net.www.protocol.jar.URLJarFile} which will extract the nested jar to
* the temp folder to that its content can be accessed.
* @param url the URL to open
* @return a {@link URLConnection} or {@code null}
*/
private URLConnection openFallbackTomcatConnection(URL url) {
String file = url.getFile();
if (isTomcatWarUrl(file)) {
file = file.substring(TOMCAT_WARFILE_PROTOCOL.length());
file = file.replaceFirst("\\*/", "!/");
try {
URLConnection connection = openConnection(new URL("jar:file:" + file));
connection.getInputStream().close();
return connection;
}
catch (IOException ex) {
}
}
return null;
}
private boolean isTomcatWarUrl(String file) {
if (file.startsWith(TOMCAT_WARFILE_PROTOCOL) || !file.contains("*/")) {
try {
URLConnection connection = new URL(file).openConnection();
if (connection.getClass().getName().startsWith("org.apache.catalina")) {
return true;
}
}
catch (Exception ex) {
}
}
return false;
}
/**
* Attempt to open a fallback connection by using a context URL captured before the
* jar handler was replaced with our own version. Since this method doesn't use
* reflection it won't trigger "illegal reflective access operation has occurred"
* warnings on Java 13+.
* @param url the URL to open
* @return a {@link URLConnection} or {@code null}
*/
private URLConnection openFallbackContextConnection(URL url) {
try {
if (jarContextUrl != null) {
return new URL(jarContextUrl, url.toExternalForm()).openConnection();
}
}
catch (Exception ex) {
}
return null;
}
/**
* Attempt to open a fallback connection by using reflection to access Java's default
* jar {@link URLStreamHandler}.
* @param url the URL to open
* @return the {@link URLConnection}
* @throws Exception if not connection could be opened
*/
private URLConnection openFallbackHandlerConnection(URL url) throws Exception {
URLStreamHandler fallbackHandler = getFallbackHandler();
return new URL(null, url.toExternalForm(), fallbackHandler).openConnection();
}
private URLStreamHandler getFallbackHandler() {
if (this.fallbackHandler != null) {
return this.fallbackHandler;
}
for (String handlerClassName : FALLBACK_HANDLERS) {
try {
Class<?> handlerClass = Class.forName(handlerClassName);
this.fallbackHandler = (URLStreamHandler) handlerClass.getDeclaredConstructor().newInstance();
return this.fallbackHandler;
}
catch (Exception ex) {
// Ignore
}
}
throw new IllegalStateException("Unable to find fallback handler");
}
private void log(boolean warning, String message, Exception cause) {
try {
Level level = warning ? Level.WARNING : Level.FINEST;
Logger.getLogger(getClass().getName()).log(level, message, cause);
}
catch (Exception ex) {
if (warning) {
System.err.println("WARNING: " + message);
}
}
}
@Override
protected void parseURL(URL context, String spec, int start, int limit) {
if (spec.regionMatches(true, 0, JAR_PROTOCOL, 0, JAR_PROTOCOL.length())) {
setFile(context, getFileFromSpec(spec.substring(start, limit)));
}
else {
setFile(context, getFileFromContext(context, spec.substring(start, limit)));
}
}
private String getFileFromSpec(String spec) {
int separatorIndex = spec.lastIndexOf("!/");
if (separatorIndex == -1) {
throw new IllegalArgumentException("No !/ in spec '" + spec + "'");
}
try {
new URL(spec.substring(0, separatorIndex));
return spec;
}
catch (MalformedURLException ex) {
throw new IllegalArgumentException("Invalid spec URL '" + spec + "'", ex);
}
}
private String getFileFromContext(URL context, String spec) {
String file = context.getFile();
if (spec.startsWith("/")) {
return trimToJarRoot(file) + SEPARATOR + spec.substring(1);
}
if (file.endsWith("/")) {
return file + spec;
}
int lastSlashIndex = file.lastIndexOf('/');
if (lastSlashIndex == -1) {
throw new IllegalArgumentException("No / found in context URL's file '" + file + "'");
}
return file.substring(0, lastSlashIndex + 1) + spec;
}
private String trimToJarRoot(String file) {
int lastSeparatorIndex = file.lastIndexOf(SEPARATOR);
if (lastSeparatorIndex == -1) {
throw new IllegalArgumentException("No !/ found in context URL's file '" + file + "'");
}
return file.substring(0, lastSeparatorIndex);
}
private void setFile(URL context, String file) {
String path = normalize(file);
String query = null;
int queryIndex = path.lastIndexOf('?');
if (queryIndex != -1) {
query = path.substring(queryIndex + 1);
path = path.substring(0, queryIndex);
}
setURL(context, JAR_PROTOCOL, null, -1, null, null, path, query, context.getRef());
}
private String normalize(String file) {
if (!file.contains(CURRENT_DIR) && !file.contains(PARENT_DIR)) {
return file;
}
int afterLastSeparatorIndex = file.lastIndexOf(SEPARATOR) + SEPARATOR.length();
String afterSeparator = file.substring(afterLastSeparatorIndex);
afterSeparator = replaceParentDir(afterSeparator);
afterSeparator = replaceCurrentDir(afterSeparator);
return file.substring(0, afterLastSeparatorIndex) + afterSeparator;
}
private String replaceParentDir(String file) {
int parentDirIndex;
while ((parentDirIndex = file.indexOf(PARENT_DIR)) >= 0) {
int precedingSlashIndex = file.lastIndexOf('/', parentDirIndex - 1);
if (precedingSlashIndex >= 0) {
file = file.substring(0, precedingSlashIndex) + file.substring(parentDirIndex + 3);
}
else {
file = file.substring(parentDirIndex + 4);
}
}
return file;
}
private String replaceCurrentDir(String file) {
return CURRENT_DIR_PATTERN.matcher(file).replaceAll("/");
}
@Override
protected int hashCode(URL u) {
return hashCode(u.getProtocol(), u.getFile());
}
private int hashCode(String protocol, String file) {
int result = (protocol != null) ? protocol.hashCode() : 0;
int separatorIndex = file.indexOf(SEPARATOR);
if (separatorIndex == -1) {
return result + file.hashCode();
}
String source = file.substring(0, separatorIndex);
String entry = canonicalize(file.substring(separatorIndex + 2));
try {
result += new URL(source).hashCode();
}
catch (MalformedURLException ex) {
result += source.hashCode();
}
result += entry.hashCode();
return result;
}
@Override
protected boolean sameFile(URL u1, URL u2) {
if (!u1.getProtocol().equals("jar") || !u2.getProtocol().equals("jar")) {
return false;
}
int separator1 = u1.getFile().indexOf(SEPARATOR);
int separator2 = u2.getFile().indexOf(SEPARATOR);
if (separator1 == -1 || separator2 == -1) {
return super.sameFile(u1, u2);
}
String nested1 = u1.getFile().substring(separator1 + SEPARATOR.length());
String nested2 = u2.getFile().substring(separator2 + SEPARATOR.length());
if (!nested1.equals(nested2)) {
String canonical1 = canonicalize(nested1);
String canonical2 = canonicalize(nested2);
if (!canonical1.equals(canonical2)) {
return false;
}
}
String root1 = u1.getFile().substring(0, separator1);
String root2 = u2.getFile().substring(0, separator2);
try {
return super.sameFile(new URL(root1), new URL(root2));
}
catch (MalformedURLException ex) {
// Continue
}
return super.sameFile(u1, u2);
}
private String canonicalize(String path) {
return SEPARATOR_PATTERN.matcher(path).replaceAll("/");
}
public JarFile getRootJarFileFromUrl(URL url) throws IOException {
String spec = url.getFile();
int separatorIndex = spec.indexOf(SEPARATOR);
if (separatorIndex == -1) {
throw new MalformedURLException("Jar URL does not contain !/ separator");
}
String name = spec.substring(0, separatorIndex);
return getRootJarFile(name);
}
private JarFile getRootJarFile(String name) throws IOException {
try {
if (!name.startsWith(FILE_PROTOCOL)) {
throw new IllegalStateException("Not a file URL");
}
File file = new File(URI.create(name));
Map<File, JarFile> cache = rootFileCache.get();
JarFile result = (cache != null) ? cache.get(file) : null;
if (result == null) {
result = new JarFile(file);
addToRootFileCache(file, result);
}
return result;
}
catch (Exception ex) {
throw new IOException("Unable to open root Jar file '" + name + "'", ex);
}
}
/**
* Add the given {@link JarFile} to the root file cache.
* @param sourceFile the source file to add
* @param jarFile the jar file.
*/
static void addToRootFileCache(File sourceFile, JarFile jarFile) {
Map<File, JarFile> cache = rootFileCache.get();
if (cache == null) {
cache = new ConcurrentHashMap<>();
rootFileCache = new SoftReference<>(cache);
}
cache.put(sourceFile, jarFile);
}
/**
* If possible, capture a URL that is configured with the original jar handler so that
* we can use it as a fallback context later. We can only do this if we know that we
* can reset the handlers after.
*/
static void captureJarContextUrl() {
if (canResetCachedUrlHandlers()) {
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
try {
System.clearProperty(PROTOCOL_HANDLER);
try {
resetCachedUrlHandlers();
jarContextUrl = new URL("jar:file:context.jar!/");
URLConnection connection = jarContextUrl.openConnection();
if (connection instanceof JarURLConnection) {
jarContextUrl = null;
}
}
catch (Exception ex) {
}
}
finally {
if (handlers == null) {
System.clearProperty(PROTOCOL_HANDLER);
}
else {
System.setProperty(PROTOCOL_HANDLER, handlers);
}
}
resetCachedUrlHandlers();
}
}
private static boolean canResetCachedUrlHandlers() {
try {
resetCachedUrlHandlers();
return true;
}
catch (Error ex) {
return false;
}
}
private static void resetCachedUrlHandlers() {
URL.setURLStreamHandlerFactory(null);
}
/**
* Set if a generic static exception can be thrown when a URL cannot be connected.
* This optimization is used during class loading to save creating lots of exceptions
* which are then swallowed.
* @param useFastConnectionExceptions if fast connection exceptions can be used.
*/
public static void setUseFastConnectionExceptions(boolean useFastConnectionExceptions) {
JarURLConnection.setUseFastExceptions(useFastConnectionExceptions);
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.woggioni.envelope.loader;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.CodeSigner;
import java.security.cert.Certificate;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
/**
* Extended variant of {@link java.util.jar.JarEntry} returned by {@link JarFile}s.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class JarEntry extends java.util.jar.JarEntry implements FileHeader {
private final int index;
private final Bytes.AsciiBytes name;
private final Bytes.AsciiBytes headerName;
private final JarFile jarFile;
private long localHeaderOffset;
private volatile JarEntryCertification certification;
JarEntry(JarFile jarFile, int index, CentralDirectoryFileHeader header, Bytes.AsciiBytes nameAlias) {
super((nameAlias != null) ? nameAlias.toString() : header.getName().toString());
this.index = index;
this.name = (nameAlias != null) ? nameAlias : header.getName();
this.headerName = header.getName();
this.jarFile = jarFile;
this.localHeaderOffset = header.getLocalHeaderOffset();
setCompressedSize(header.getCompressedSize());
setMethod(header.getMethod());
setCrc(header.getCrc());
setComment(header.getComment().toString());
setSize(header.getSize());
setTime(header.getTime());
if (header.hasExtra()) {
setExtra(header.getExtra());
}
}
int getIndex() {
return this.index;
}
Bytes.AsciiBytes getAsciiBytesName() {
return this.name;
}
@Override
public boolean hasName(CharSequence name, char suffix) {
return this.headerName.matches(name, suffix);
}
/**
* Return a {@link URL} for this {@link JarEntry}.
* @return the URL for the entry
* @throws MalformedURLException if the URL is not valid
*/
URL getUrl() throws MalformedURLException {
return new URL(this.jarFile.getUrl(), getName());
}
@Override
public Attributes getAttributes() throws IOException {
Manifest manifest = this.jarFile.getManifest();
return (manifest != null) ? manifest.getAttributes(getName()) : null;
}
@Override
public Certificate[] getCertificates() {
return getCertification().getCertificates();
}
@Override
public CodeSigner[] getCodeSigners() {
return getCertification().getCodeSigners();
}
private JarEntryCertification getCertification() {
if (!this.jarFile.isSigned()) {
return JarEntryCertification.NONE;
}
JarEntryCertification certification = this.certification;
if (certification == null) {
certification = this.jarFile.getCertification(this);
this.certification = certification;
}
return certification;
}
@Override
public long getLocalHeaderOffset() {
return this.localHeaderOffset;
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.woggioni.envelope.loader;
import java.security.CodeSigner;
import java.security.cert.Certificate;
/**
* {@link Certificate} and {@link CodeSigner} details for a {@link JarEntry} from a signed
* {@link JarFile}.
*
* @author Phillip Webb
*/
class JarEntryCertification {
static final JarEntryCertification NONE = new JarEntryCertification(null, null);
private final Certificate[] certificates;
private final CodeSigner[] codeSigners;
JarEntryCertification(Certificate[] certificates, CodeSigner[] codeSigners) {
this.certificates = certificates;
this.codeSigners = codeSigners;
}
Certificate[] getCertificates() {
return (this.certificates != null) ? this.certificates.clone() : null;
}
CodeSigner[] getCodeSigners() {
return (this.codeSigners != null) ? this.codeSigners.clone() : null;
}
static JarEntryCertification from(java.util.jar.JarEntry certifiedEntry) {
Certificate[] certificates = (certifiedEntry != null) ? certifiedEntry.getCertificates() : null;
CodeSigner[] codeSigners = (certifiedEntry != null) ? certifiedEntry.getCodeSigners() : null;
if (certificates == null && codeSigners == null) {
return NONE;
}
return new JarEntryCertification(certificates, codeSigners);
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.woggioni.envelope.loader;
/**
* Interface that can be used to filter and optionally rename jar entries.
*
* @author Phillip Webb
*/
interface JarEntryFilter {
/**
* Apply the jar entry filter.
* @param name the current entry name. This may be different that the original entry
* name if a previous filter has been applied
* @return the new name of the entry or {@code null} if the entry should not be
* included.
*/
Bytes.AsciiBytes apply(Bytes.AsciiBytes name);
}

View File

@@ -0,0 +1,490 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.woggioni.envelope.loader;
import java.io.File;
import java.io.FilePermission;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.security.Permission;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Supplier;
import java.util.jar.Manifest;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import java.util.zip.ZipEntry;
/**
* Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but
* offers the following additional functionality.
* <ul>
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based
* on any directory entry.</li>
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
* embedded JAR files (as long as their entry is not compressed).</li>
* </ul>
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.0.0
*/
public class JarFile extends AbstractJarFile implements Iterable<java.util.jar.JarEntry> {
private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
private static final String HANDLERS_PACKAGE = "net.woggioni.xclassloader.jar";
private static final Bytes.AsciiBytes META_INF = new Bytes.AsciiBytes("META-INF/");
private static final Bytes.AsciiBytes SIGNATURE_FILE_EXTENSION = new Bytes.AsciiBytes(".SF");
private static final String READ_ACTION = "read";
private final RandomAccessDataFile rootFile;
private final String pathFromRoot;
private final RandomAccessData data;
private final JarFileType type;
private URL url;
private String urlString;
private JarFileEntries entries;
private Supplier<Manifest> manifestSupplier;
private SoftReference<Manifest> manifest;
private boolean signed;
private String comment;
private volatile boolean closed;
private volatile JarFileWrapper wrapper;
private final List<JarFile> nestedJars = Collections.synchronizedList(new ArrayList<>());
/**
* Create a new {@link JarFile} backed by the specified file.
* @param file the root jar file
* @throws IOException if the file cannot be read
*/
public JarFile(File file) throws IOException {
this(new RandomAccessDataFile(file));
}
/**
* Create a new {@link JarFile} backed by the specified file.
* @param file the root jar file
* @throws IOException if the file cannot be read
*/
JarFile(RandomAccessDataFile file) throws IOException {
this(file, "", file, JarFileType.DIRECT);
}
/**
* Private constructor used to create a new {@link JarFile} either directly or from a
* nested entry.
* @param rootFile the root jar file
* @param pathFromRoot the name of this file
* @param data the underlying data
* @param type the type of the jar file
* @throws IOException if the file cannot be read
*/
private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data, JarFileType type)
throws IOException {
this(rootFile, pathFromRoot, data, null, type, null);
}
private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data, JarEntryFilter filter,
JarFileType type, Supplier<Manifest> manifestSupplier) throws IOException {
super(rootFile.getFile());
this.rootFile = rootFile;
this.pathFromRoot = pathFromRoot;
CentralDirectoryParser parser = new CentralDirectoryParser();
this.entries = parser.addVisitor(new JarFileEntries(this, filter));
this.type = type;
parser.addVisitor(centralDirectoryVisitor());
try {
this.data = parser.parse(data, filter == null);
}
catch (RuntimeException ex) {
try {
close();
}
catch (IOException ioex) {
}
throw ex;
}
this.manifestSupplier = (manifestSupplier != null) ? manifestSupplier : () -> {
try (InputStream inputStream = getInputStream(MANIFEST_NAME)) {
if (inputStream == null) {
return null;
}
return new Manifest(inputStream);
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
};
}
private CentralDirectoryVisitor centralDirectoryVisitor() {
return new CentralDirectoryVisitor() {
@Override
public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) {
JarFile.this.comment = endRecord.getComment();
}
@Override
public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) {
Bytes.AsciiBytes name = fileHeader.getName();
if (name.startsWith(META_INF) && name.endsWith(SIGNATURE_FILE_EXTENSION)) {
JarFile.this.signed = true;
}
}
@Override
public void visitEnd() {
}
};
}
JarFileWrapper getWrapper() throws IOException {
JarFileWrapper wrapper = this.wrapper;
if (wrapper == null) {
synchronized (this) {
if (this.wrapper != null) {
return this.wrapper;
}
wrapper = new JarFileWrapper(this);
this.wrapper = wrapper;
}
}
return wrapper;
}
@Override
Permission getPermission() {
return new FilePermission(this.rootFile.getFile().getPath(), READ_ACTION);
}
protected final RandomAccessDataFile getRootJarFile() {
return this.rootFile;
}
RandomAccessData getData() {
return this.data;
}
@Override
public Manifest getManifest() throws IOException {
Manifest manifest = (this.manifest != null) ? this.manifest.get() : null;
if (manifest == null) {
try {
manifest = this.manifestSupplier.get();
}
catch (RuntimeException ex) {
throw new IOException(ex);
}
this.manifest = new SoftReference<>(manifest);
}
return manifest;
}
@Override
public Enumeration<java.util.jar.JarEntry> entries() {
return new JarEntryEnumeration(this.entries.iterator());
}
@Override
public Stream<java.util.jar.JarEntry> stream() {
Spliterator<java.util.jar.JarEntry> spliterator = Spliterators.spliterator(iterator(), size(),
Spliterator.ORDERED | Spliterator.DISTINCT | Spliterator.IMMUTABLE | Spliterator.NONNULL);
return StreamSupport.stream(spliterator, false);
}
/**
* Return an iterator for the contained entries.
* @since 2.3.0
* @see Iterable#iterator()
*/
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public Iterator<java.util.jar.JarEntry> iterator() {
return (Iterator) this.entries.iterator(this::ensureOpen);
}
public JarEntry getJarEntry(CharSequence name) {
return this.entries.getEntry(name);
}
@Override
public JarEntry getJarEntry(String name) {
return (JarEntry) getEntry(name);
}
public boolean containsEntry(String name) {
return this.entries.containsEntry(name);
}
@Override
public ZipEntry getEntry(String name) {
ensureOpen();
return this.entries.getEntry(name);
}
@Override
InputStream getInputStream() throws IOException {
return this.data.getInputStream();
}
@Override
public synchronized InputStream getInputStream(ZipEntry entry) throws IOException {
ensureOpen();
if (entry instanceof JarEntry) {
return this.entries.getInputStream((JarEntry) entry);
}
return getInputStream((entry != null) ? entry.getName() : null);
}
InputStream getInputStream(String name) throws IOException {
return this.entries.getInputStream(name);
}
/**
* Return a nested {@link JarFile} loaded from the specified entry.
* @param entry the zip entry
* @return a {@link JarFile} for the entry
* @throws IOException if the nested jar file cannot be read
*/
public synchronized JarFile getNestedJarFile(ZipEntry entry) throws IOException {
return getNestedJarFile((JarEntry) entry);
}
/**
* Return a nested {@link JarFile} loaded from the specified entry.
* @param entry the zip entry
* @return a {@link JarFile} for the entry
* @throws IOException if the nested jar file cannot be read
*/
public synchronized JarFile getNestedJarFile(JarEntry entry) throws IOException {
try {
return createJarFileFromEntry(entry);
}
catch (Exception ex) {
throw new IOException("Unable to open nested jar file '" + entry.getName() + "'", ex);
}
}
private JarFile createJarFileFromEntry(JarEntry entry) throws IOException {
if (entry.isDirectory()) {
return createJarFileFromDirectoryEntry(entry);
}
return createJarFileFromFileEntry(entry);
}
private JarFile createJarFileFromDirectoryEntry(JarEntry entry) throws IOException {
Bytes.AsciiBytes name = entry.getAsciiBytesName();
JarEntryFilter filter = (candidate) -> {
if (candidate.startsWith(name) && !candidate.equals(name)) {
return candidate.substring(name.length());
}
return null;
};
return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName().substring(0, name.length() - 1),
this.data, filter, JarFileType.NESTED_DIRECTORY, this.manifestSupplier);
}
private JarFile createJarFileFromFileEntry(JarEntry entry) throws IOException {
if (entry.getMethod() != ZipEntry.STORED) {
throw new IllegalStateException(
"Unable to open nested entry '" + entry.getName() + "'. It has been compressed and nested "
+ "jar files must be stored without compression. Please check the "
+ "mechanism used to create your executable jar file");
}
RandomAccessData entryData = this.entries.getEntryData(entry.getName());
JarFile nestedJar = new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName(), entryData,
JarFileType.NESTED_JAR);
this.nestedJars.add(nestedJar);
return nestedJar;
}
@Override
public String getComment() {
ensureOpen();
return this.comment;
}
@Override
public int size() {
ensureOpen();
return this.entries.getSize();
}
@Override
public void close() throws IOException {
if (this.closed) {
return;
}
synchronized (this) {
super.close();
if (this.type == JarFileType.DIRECT) {
this.rootFile.close();
}
if (this.wrapper != null) {
this.wrapper.close();
}
for (JarFile nestedJar : this.nestedJars) {
nestedJar.close();
}
this.closed = true;
}
}
private void ensureOpen() {
if (this.closed) {
throw new IllegalStateException("zip file closed");
}
}
boolean isClosed() {
return this.closed;
}
String getUrlString() throws MalformedURLException {
if (this.urlString == null) {
this.urlString = getUrl().toString();
}
return this.urlString;
}
@Override
public URL getUrl() throws MalformedURLException {
if (this.url == null) {
String file = this.rootFile.getFile().toURI() + this.pathFromRoot + "!/";
file = file.replace("file:////", "file://"); // Fix UNC paths
this.url = new URL("jar", "", -1, file, new Handler(this));
}
return this.url;
}
@Override
public String toString() {
return getName();
}
@Override
public String getName() {
return this.rootFile.getFile() + this.pathFromRoot;
}
boolean isSigned() {
return this.signed;
}
JarEntryCertification getCertification(JarEntry entry) {
try {
return this.entries.getCertification(entry);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
public void clearCache() {
this.entries.clearCache();
}
protected String getPathFromRoot() {
return this.pathFromRoot;
}
@Override
JarFileType getType() {
return this.type;
}
/**
* Register a {@literal 'java.protocol.handler.pkgs'} property so that a
* {@link URLStreamHandler} will be located to deal with jar URLs.
*/
public static void registerUrlProtocolHandler() {
Handler.captureJarContextUrl();
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
System.setProperty(PROTOCOL_HANDLER,
((handlers == null || handlers.isEmpty()) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
resetCachedUrlHandlers();
}
/**
* Reset any cached handlers just in case a jar protocol has already been used. We
* reset the handler by trying to set a null {@link URLStreamHandlerFactory} which
* should have no effect other than clearing the handlers cache.
*/
private static void resetCachedUrlHandlers() {
try {
URL.setURLStreamHandlerFactory(null);
}
catch (Error ex) {
// Ignore
}
}
/**
* An {@link Enumeration} on {@linkplain java.util.jar.JarEntry jar entries}.
*/
private static class JarEntryEnumeration implements Enumeration<java.util.jar.JarEntry> {
private final Iterator<JarEntry> iterator;
JarEntryEnumeration(Iterator<JarEntry> iterator) {
this.iterator = iterator;
}
@Override
public boolean hasMoreElements() {
return this.iterator.hasNext();
}
@Override
public java.util.jar.JarEntry nextElement() {
return this.iterator.next();
}
}
}

View File

@@ -0,0 +1,498 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.woggioni.envelope.loader;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
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;
/**
* Provides access to entries from a {@link JarFile}. In order to reduce memory
* consumption entry details are stored using arrays. The {@code hashCodes} array stores
* the hash code of the entry name, the {@code centralDirectoryOffsets} provides the
* offset to the central directory record and {@code positions} provides the original
* order position of the entry. The arrays are stored in hashCode order so that a binary
* search can be used to find a name.
* <p>
* A typical Spring Boot application will have somewhere in the region of 10,500 entries
* which should consume about 122K.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
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 = javaSpecificationVersion();
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<Integer, FileHeader> entriesCache = Collections
.synchronizedMap(new LinkedHashMap<Integer, FileHeader>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, FileHeader> eldest) {
return size() >= ENTRY_CACHE_SIZE;
}
});
JarFileEntries(JarFile jarFile, JarEntryFilter filter) {
this.jarFile = jarFile;
this.filter = filter;
}
@Override
public void visitStart(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(CentralDirectoryFileHeader fileHeader, long dataOffset) {
Bytes.AsciiBytes name = applyFilter(fileHeader.getName());
if (name != null) {
add(name, dataOffset);
}
}
private void add(Bytes.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) {
// Quick sort algorithm, uses hashCodes as the source but sorts all arrays
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<JarEntry> iterator() {
return new EntryIterator(NO_VALIDATION);
}
Iterator<JarEntry> 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(FileHeader entry) throws IOException {
// aspectjrt-1.7.4.jar has a different ext bytes length in the
// local directory to the central directory. We need to re-read
// here to skip them
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 extends FileHeader> T getEntry(CharSequence name, Class<T> type, boolean cacheEntry) {
T entry = doGetEntry(name, type, cacheEntry, null);
if (!isMetaInfEntry(name) && isMultiReleaseJar()) {
int version = RUNTIME_VERSION;
Bytes.AsciiBytes nameAlias = (entry instanceof JarEntry) ? ((JarEntry) entry).getAsciiBytesName()
: new Bytes.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(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 extends FileHeader> T doGetEntry(CharSequence name, Class<T> type, boolean cacheEntry,
Bytes.AsciiBytes nameAlias) {
int hashCode = Bytes.AsciiBytes.hashCode(name);
T entry = getEntry(hashCode, name, NO_SUFFIX, type, cacheEntry, nameAlias);
if (entry == null) {
hashCode = Bytes.AsciiBytes.hashCode(hashCode, SLASH);
entry = getEntry(hashCode, name, SLASH, type, cacheEntry, nameAlias);
}
return entry;
}
private <T extends FileHeader> T getEntry(int hashCode, CharSequence name, char suffix, Class<T> type,
boolean cacheEntry, Bytes.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 <T extends FileHeader> T getEntry(int index, Class<T> type, boolean cacheEntry, Bytes.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 Bytes.AsciiBytes applyFilter(Bytes.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];
// We fall back to use JarInputStream to obtain the certs. This isn't that
// fast, but hopefully doesn't happen too often.
try (JarInputStream certifiedJarStream = new JarInputStream(this.jarFile.getData().getInputStream())) {
java.util.jar.JarEntry certifiedEntry = null;
while ((certifiedEntry = certifiedJarStream.getNextJarEntry()) != null) {
// Entry must be closed to trigger a read and set entry certificates
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 = Bytes.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[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
private static void swap(long[] array, int i, int j) {
long temp = array[i];
array[i] = array[j];
array[j] = temp;
}
private static int javaSpecificationVersion() {
float v = Float.parseFloat(System.getProperty("java.specification.version"));
if(v < 2) {
return ((int) (v * 10)) - 10;
} else {
return (int) v;
}
}
/**
* Iterator for contained entries.
*/
private final class EntryIterator implements Iterator<JarEntry> {
private final Runnable validator;
private int index = 0;
private EntryIterator(Runnable validator) {
this.validator = validator;
validator.run();
}
@Override
public boolean hasNext() {
this.validator.run();
return this.index < JarFileEntries.this.size;
}
@Override
public 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);
}
}
/**
* Interface to manage offsets to central directory records. Regular zip files are
* backed by an {@code int[]} based implementation, Zip64 files are backed by a
* {@code long[]} and will consume more memory.
*/
private interface Offsets {
void set(int index, long value);
long get(int index);
void swap(int i, int j);
static Offsets from(CentralDirectoryEndRecord endRecord) {
int size = endRecord.getNumberOfRecords();
return endRecord.isZip64() ? new Zip64Offsets(size) : new ZipOffsets(size);
}
}
/**
* {@link Offsets} implementation for regular zip files.
*/
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];
}
}
/**
* {@link Offsets} implementation for zip64 files.
*/
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];
}
}
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.woggioni.envelope.loader;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.Permission;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.Manifest;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
/**
* A wrapper used to create a copy of a {@link JarFile} so that it can be safely closed
* without closing the original.
*
* @author Phillip Webb
*/
class JarFileWrapper extends AbstractJarFile {
private final JarFile parent;
JarFileWrapper(JarFile parent) throws IOException {
super(parent.getRootJarFile().getFile());
this.parent = parent;
}
@Override
URL getUrl() throws MalformedURLException {
return this.parent.getUrl();
}
@Override
JarFileType getType() {
return this.parent.getType();
}
@Override
Permission getPermission() {
return this.parent.getPermission();
}
@Override
public Manifest getManifest() throws IOException {
return this.parent.getManifest();
}
@Override
public Enumeration<JarEntry> entries() {
return this.parent.entries();
}
@Override
public Stream<JarEntry> stream() {
return this.parent.stream();
}
@Override
public JarEntry getJarEntry(String name) {
return this.parent.getJarEntry(name);
}
@Override
public ZipEntry getEntry(String name) {
return this.parent.getEntry(name);
}
@Override
InputStream getInputStream() throws IOException {
return this.parent.getInputStream();
}
@Override
public synchronized InputStream getInputStream(ZipEntry ze) throws IOException {
return this.parent.getInputStream(ze);
}
@Override
public String getComment() {
return this.parent.getComment();
}
@Override
public int size() {
return this.parent.size();
}
@Override
public String toString() {
return this.parent.toString();
}
@Override
public String getName() {
return this.parent.getName();
}
static JarFile unwrap(java.util.jar.JarFile jarFile) {
if (jarFile instanceof JarFile) {
return (JarFile) jarFile;
}
if (jarFile instanceof JarFileWrapper) {
return unwrap(((JarFileWrapper) jarFile).parent);
}
throw new IllegalStateException("Not a JarFile or Wrapper");
}
}

View File

@@ -0,0 +1,407 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.woggioni.envelope.loader;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.net.URLStreamHandler;
import java.security.Permission;
/**
* {@link java.net.JarURLConnection} used to support {@link JarFile#getUrl()}.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Rostyslav Dudka
*/
final class JarURLConnection extends java.net.JarURLConnection {
private static ThreadLocal<Boolean> useFastExceptions = new ThreadLocal<>();
private static final FileNotFoundException FILE_NOT_FOUND_EXCEPTION = new FileNotFoundException(
"Jar file or entry not found");
private static final IllegalStateException NOT_FOUND_CONNECTION_EXCEPTION = new IllegalStateException(
FILE_NOT_FOUND_EXCEPTION);
private static final String SEPARATOR = "!/";
private static final URL EMPTY_JAR_URL;
static {
try {
EMPTY_JAR_URL = new URL("jar:", null, 0, "file:!/", new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL u) throws IOException {
// Stub URLStreamHandler to prevent the wrong JAR Handler from being
// Instantiated and cached.
return null;
}
});
}
catch (MalformedURLException ex) {
throw new IllegalStateException(ex);
}
}
private static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName(new Bytes.StringSequence(""));
private static final JarURLConnection NOT_FOUND_CONNECTION = JarURLConnection.notFound();
private final AbstractJarFile jarFile;
private Permission permission;
private URL jarFileUrl;
private final JarEntryName jarEntryName;
private java.util.jar.JarEntry jarEntry;
private JarURLConnection(URL url, AbstractJarFile jarFile, JarEntryName jarEntryName) throws IOException {
// What we pass to super is ultimately ignored
super(EMPTY_JAR_URL);
this.url = url;
this.jarFile = jarFile;
this.jarEntryName = jarEntryName;
}
@Override
public void connect() throws IOException {
if (this.jarFile == null) {
throw FILE_NOT_FOUND_EXCEPTION;
}
if (!this.jarEntryName.isEmpty() && this.jarEntry == null) {
this.jarEntry = this.jarFile.getJarEntry(getEntryName());
if (this.jarEntry == null) {
throwFileNotFound(this.jarEntryName, this.jarFile);
}
}
this.connected = true;
}
@Override
public java.util.jar.JarFile getJarFile() throws IOException {
connect();
return this.jarFile;
}
@Override
public URL getJarFileURL() {
if (this.jarFile == null) {
throw NOT_FOUND_CONNECTION_EXCEPTION;
}
if (this.jarFileUrl == null) {
this.jarFileUrl = buildJarFileUrl();
}
return this.jarFileUrl;
}
private URL buildJarFileUrl() {
try {
String spec = this.jarFile.getUrl().getFile();
if (spec.endsWith(SEPARATOR)) {
spec = spec.substring(0, spec.length() - SEPARATOR.length());
}
if (!spec.contains(SEPARATOR)) {
return new URL(spec);
}
return new URL("jar:" + spec);
}
catch (MalformedURLException ex) {
throw new IllegalStateException(ex);
}
}
@Override
public java.util.jar.JarEntry getJarEntry() throws IOException {
if (this.jarEntryName == null || this.jarEntryName.isEmpty()) {
return null;
}
connect();
return this.jarEntry;
}
@Override
public String getEntryName() {
if (this.jarFile == null) {
throw NOT_FOUND_CONNECTION_EXCEPTION;
}
return this.jarEntryName.toString();
}
@Override
public InputStream getInputStream() throws IOException {
if (this.jarFile == null) {
throw FILE_NOT_FOUND_EXCEPTION;
}
if (this.jarEntryName.isEmpty() && this.jarFile.getType() == JarFile.JarFileType.DIRECT) {
throw new IOException("no entry name specified");
}
connect();
InputStream inputStream = (this.jarEntryName.isEmpty() ? this.jarFile.getInputStream()
: this.jarFile.getInputStream(this.jarEntry));
if (inputStream == null) {
throwFileNotFound(this.jarEntryName, this.jarFile);
}
return new ConnectionInputStream(inputStream);
}
private void throwFileNotFound(Object entry, AbstractJarFile jarFile) throws FileNotFoundException {
if (Boolean.TRUE.equals(useFastExceptions.get())) {
throw FILE_NOT_FOUND_EXCEPTION;
}
throw new FileNotFoundException("JAR entry " + entry + " not found in " + jarFile.getName());
}
@Override
public int getContentLength() {
long length = getContentLengthLong();
if (length > Integer.MAX_VALUE) {
return -1;
}
return (int) length;
}
@Override
public long getContentLengthLong() {
if (this.jarFile == null) {
return -1;
}
try {
if (this.jarEntryName.isEmpty()) {
return this.jarFile.size();
}
java.util.jar.JarEntry entry = getJarEntry();
return (entry != null) ? (int) entry.getSize() : -1;
}
catch (IOException ex) {
return -1;
}
}
@Override
public Object getContent() throws IOException {
connect();
return this.jarEntryName.isEmpty() ? this.jarFile : super.getContent();
}
@Override
public String getContentType() {
return (this.jarEntryName != null) ? this.jarEntryName.getContentType() : null;
}
@Override
public Permission getPermission() throws IOException {
if (this.jarFile == null) {
throw FILE_NOT_FOUND_EXCEPTION;
}
if (this.permission == null) {
this.permission = this.jarFile.getPermission();
}
return this.permission;
}
@Override
public long getLastModified() {
if (this.jarFile == null || this.jarEntryName.isEmpty()) {
return 0;
}
try {
java.util.jar.JarEntry entry = getJarEntry();
return (entry != null) ? entry.getTime() : 0;
}
catch (IOException ex) {
return 0;
}
}
static void setUseFastExceptions(boolean useFastExceptions) {
JarURLConnection.useFastExceptions.set(useFastExceptions);
}
static JarURLConnection get(URL url, JarFile jarFile) throws IOException {
Bytes.StringSequence spec = new Bytes.StringSequence(url.getFile());
int index = indexOfRootSpec(spec, jarFile.getPathFromRoot());
if (index == -1) {
return (Boolean.TRUE.equals(useFastExceptions.get()) ? NOT_FOUND_CONNECTION
: new JarURLConnection(url, null, EMPTY_JAR_ENTRY_NAME));
}
int separator;
while ((separator = spec.indexOf(SEPARATOR, index)) > 0) {
JarEntryName entryName = JarEntryName.get(spec.subSequence(index, separator));
JarEntry jarEntry = jarFile.getJarEntry(entryName.toCharSequence());
if (jarEntry == null) {
return JarURLConnection.notFound(jarFile, entryName);
}
jarFile = jarFile.getNestedJarFile(jarEntry);
index = separator + SEPARATOR.length();
}
JarEntryName jarEntryName = JarEntryName.get(spec, index);
if (Boolean.TRUE.equals(useFastExceptions.get()) && !jarEntryName.isEmpty()
&& !jarFile.containsEntry(jarEntryName.toString())) {
return NOT_FOUND_CONNECTION;
}
return new JarURLConnection(url, jarFile.getWrapper(), jarEntryName);
}
private static int indexOfRootSpec(Bytes.StringSequence file, String pathFromRoot) {
int separatorIndex = file.indexOf(SEPARATOR);
if (separatorIndex < 0 || !file.startsWith(pathFromRoot, separatorIndex)) {
return -1;
}
return separatorIndex + SEPARATOR.length() + pathFromRoot.length();
}
private static JarURLConnection notFound() {
try {
return notFound(null, null);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
private static JarURLConnection notFound(JarFile jarFile, JarEntryName jarEntryName) throws IOException {
if (Boolean.TRUE.equals(useFastExceptions.get())) {
return NOT_FOUND_CONNECTION;
}
return new JarURLConnection(null, jarFile, jarEntryName);
}
private class ConnectionInputStream extends FilterInputStream {
ConnectionInputStream(InputStream in) {
super(in);
}
@Override
public void close() throws IOException {
JarURLConnection.this.jarFile.close();
}
}
/**
* A JarEntryName parsed from a URL String.
*/
static class JarEntryName {
private final Bytes.StringSequence name;
private String contentType;
JarEntryName(Bytes.StringSequence spec) {
this.name = decode(spec);
}
private Bytes.StringSequence decode(Bytes.StringSequence source) {
if (source.isEmpty() || (source.indexOf('%') < 0)) {
return source;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length());
write(source.toString(), bos);
// AsciiBytes is what is used to store the JarEntries so make it symmetric
return new Bytes.StringSequence(Bytes.AsciiBytes.toString(bos.toByteArray()));
}
private void write(String source, ByteArrayOutputStream outputStream) {
int length = source.length();
for (int i = 0; i < length; i++) {
int c = source.charAt(i);
if (c > 127) {
try {
String encoded = URLEncoder.encode(String.valueOf((char) c), "UTF-8");
write(encoded, outputStream);
}
catch (UnsupportedEncodingException ex) {
throw new IllegalStateException(ex);
}
}
else {
if (c == '%') {
if ((i + 2) >= length) {
throw new IllegalArgumentException(
"Invalid encoded sequence \"" + source.substring(i) + "\"");
}
c = decodeEscapeSequence(source, i);
i += 2;
}
outputStream.write(c);
}
}
}
private char decodeEscapeSequence(String source, int i) {
int hi = Character.digit(source.charAt(i + 1), 16);
int lo = Character.digit(source.charAt(i + 2), 16);
if (hi == -1 || lo == -1) {
throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\"");
}
return ((char) ((hi << 4) + lo));
}
CharSequence toCharSequence() {
return this.name;
}
@Override
public String toString() {
return this.name.toString();
}
boolean isEmpty() {
return this.name.isEmpty();
}
String getContentType() {
if (this.contentType == null) {
this.contentType = deduceContentType();
}
return this.contentType;
}
private String deduceContentType() {
// Guess the content type, don't bother with streams as mark is not supported
String type = isEmpty() ? "x-java/jar" : null;
type = (type != null) ? type : guessContentTypeFromName(toString());
type = (type != null) ? type : "content/unknown";
return type;
}
static JarEntryName get(Bytes.StringSequence spec) {
return get(spec, 0);
}
static JarEntryName get(Bytes.StringSequence spec, int beginIndex) {
if (spec.length() <= beginIndex) {
return EMPTY_JAR_ENTRY_NAME;
}
return new JarEntryName(spec.subSequence(beginIndex));
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.woggioni.envelope.loader;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
/**
* Interface that provides read-only random access to some underlying data.
* Implementations must allow concurrent reads in a thread-safe manner.
*
* @author Phillip Webb
* @since 1.0.0
*/
public interface RandomAccessData {
/**
* Returns an {@link InputStream} that can be used to read the underlying data. The
* caller is responsible close the underlying stream.
* @return a new input stream that can be used to read the underlying data.
* @throws IOException if the stream cannot be opened
*/
InputStream getInputStream() throws IOException;
/**
* Returns a new {@link RandomAccessData} for a specific subsection of this data.
* @param offset the offset of the subsection
* @param length the length of the subsection
* @return the subsection data
*/
RandomAccessData getSubsection(long offset, long length);
/**
* Reads all the data and returns it as a byte array.
* @return the data
* @throws IOException if the data cannot be read
*/
byte[] read() throws IOException;
/**
* Reads the {@code length} bytes of data starting at the given {@code offset}.
* @param offset the offset from which data should be read
* @param length the number of bytes to be read
* @return the data
* @throws IOException if the data cannot be read
* @throws IndexOutOfBoundsException if offset is beyond the end of the file or
* subsection
* @throws EOFException if offset plus length is greater than the length of the file
* or subsection
*/
byte[] read(long offset, long length) throws IOException;
/**
* Returns the size of the data.
* @return the size
*/
long getSize();
}

View File

@@ -0,0 +1,260 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.woggioni.envelope.loader;
import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
/**
* {@link RandomAccessData} implementation backed by a {@link RandomAccessFile}.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.0.0
*/
public class RandomAccessDataFile implements RandomAccessData {
private final FileAccess fileAccess;
private final long offset;
private final long length;
/**
* Create a new {@link RandomAccessDataFile} backed by the specified file.
* @param file the underlying file
* @throws IllegalArgumentException if the file is null or does not exist
*/
public RandomAccessDataFile(File file) {
if (file == null) {
throw new IllegalArgumentException("File must not be null");
}
this.fileAccess = new FileAccess(file);
this.offset = 0L;
this.length = file.length();
}
/**
* Private constructor used to create a {@link #getSubsection(long, long) subsection}.
* @param fileAccess provides access to the underlying file
* @param offset the offset of the section
* @param length the length of the section
*/
private RandomAccessDataFile(FileAccess fileAccess, long offset, long length) {
this.fileAccess = fileAccess;
this.offset = offset;
this.length = length;
}
/**
* Returns the underlying File.
* @return the underlying file
*/
public File getFile() {
return this.fileAccess.file;
}
@Override
public InputStream getInputStream() throws IOException {
return new DataInputStream();
}
@Override
public RandomAccessData getSubsection(long offset, long length) {
if (offset < 0 || length < 0 || offset + length > this.length) {
throw new IndexOutOfBoundsException();
}
return new RandomAccessDataFile(this.fileAccess, this.offset + offset, length);
}
@Override
public byte[] read() throws IOException {
return read(0, this.length);
}
@Override
public byte[] read(long offset, long length) throws IOException {
if (offset > this.length) {
throw new IndexOutOfBoundsException();
}
if (offset + length > this.length) {
throw new EOFException();
}
byte[] bytes = new byte[(int) length];
read(bytes, offset, 0, bytes.length);
return bytes;
}
private int readByte(long position) throws IOException {
if (position >= this.length) {
return -1;
}
return this.fileAccess.readByte(this.offset + position);
}
private int read(byte[] bytes, long position, int offset, int length) throws IOException {
if (position > this.length) {
return -1;
}
return this.fileAccess.read(bytes, this.offset + position, offset, length);
}
@Override
public long getSize() {
return this.length;
}
public void close() throws IOException {
this.fileAccess.close();
}
/**
* {@link InputStream} implementation for the {@link RandomAccessDataFile}.
*/
private class DataInputStream extends InputStream {
private int position;
@Override
public int read() throws IOException {
int read = RandomAccessDataFile.this.readByte(this.position);
if (read > -1) {
moveOn(1);
}
return read;
}
@Override
public int read(byte[] b) throws IOException {
return read(b, 0, (b != null) ? b.length : 0);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException("Bytes must not be null");
}
return doRead(b, off, len);
}
/**
* Perform the actual read.
* @param b the bytes to read or {@code null} when reading a single byte
* @param off the offset of the byte array
* @param len the length of data to read
* @return the number of bytes read into {@code b} or the actual read byte if
* {@code b} is {@code null}. Returns -1 when the end of the stream is reached
* @throws IOException in case of I/O errors
*/
int doRead(byte[] b, int off, int len) throws IOException {
if (len == 0) {
return 0;
}
int cappedLen = cap(len);
if (cappedLen <= 0) {
return -1;
}
return (int) moveOn(RandomAccessDataFile.this.read(b, this.position, off, cappedLen));
}
@Override
public long skip(long n) throws IOException {
return (n <= 0) ? 0 : moveOn(cap(n));
}
@Override
public int available() throws IOException {
return (int) RandomAccessDataFile.this.length - this.position;
}
/**
* Cap the specified value such that it cannot exceed the number of bytes
* remaining.
* @param n the value to cap
* @return the capped value
*/
private int cap(long n) {
return (int) Math.min(RandomAccessDataFile.this.length - this.position, n);
}
/**
* Move the stream position forwards the specified amount.
* @param amount the amount to move
* @return the amount moved
*/
private long moveOn(int amount) {
this.position += amount;
return amount;
}
}
private static final class FileAccess {
private final Object monitor = new Object();
private final File file;
private RandomAccessFile randomAccessFile;
private FileAccess(File file) {
this.file = file;
openIfNecessary();
}
private int read(byte[] bytes, long position, int offset, int length) throws IOException {
synchronized (this.monitor) {
openIfNecessary();
this.randomAccessFile.seek(position);
return this.randomAccessFile.read(bytes, offset, length);
}
}
private void openIfNecessary() {
if (this.randomAccessFile == null) {
try {
this.randomAccessFile = new RandomAccessFile(this.file, "r");
}
catch (FileNotFoundException ex) {
throw new IllegalArgumentException(
String.format("File %s must exist", this.file.getAbsolutePath()));
}
}
}
private void close() throws IOException {
synchronized (this.monitor) {
if (this.randomAccessFile != null) {
this.randomAccessFile.close();
this.randomAccessFile = null;
}
}
}
private int readByte(long position) throws IOException {
synchronized (this.monitor) {
openIfNecessary();
this.randomAccessFile.seek(position);
return this.randomAccessFile.read();
}
}
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.woggioni.envelope.loader;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
/**
* {@link InflaterInputStream} that supports the writing of an extra "dummy" byte (which
* is required with JDK 6) and returns accurate available() results.
*
* @author Phillip Webb
*/
class ZipInflaterInputStream extends InflaterInputStream {
private int available;
private boolean extraBytesWritten;
ZipInflaterInputStream(InputStream inputStream, int size) {
super(inputStream, new Inflater(true), getInflaterBufferSize(size));
this.available = size;
}
@Override
public int available() throws IOException {
if (this.available < 0) {
return super.available();
}
return this.available;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int result = super.read(b, off, len);
if (result != -1) {
this.available -= result;
}
return result;
}
@Override
public void close() throws IOException {
super.close();
this.inf.end();
}
@Override
protected void fill() throws IOException {
try {
super.fill();
}
catch (EOFException ex) {
if (this.extraBytesWritten) {
throw ex;
}
this.len = 1;
this.buf[0] = 0x0;
this.extraBytesWritten = true;
this.inf.setInput(this.buf, 0, this.len);
}
}
private static int getInflaterBufferSize(long size) {
size += 2; // inflater likes some space
size = (size > 65536) ? 8192 : size;
size = (size <= 0) ? 4096 : size;
return (int) size;
}
}

View File

@@ -0,0 +1,4 @@
/**
* Support for loading and manipulating JAR/WAR files.
*/
package net.woggioni.envelope.loader;

View File

@@ -0,0 +1,5 @@
module net.woggioni.envelope.loader {
requires java.logging;
requires static lombok;
exports net.woggioni.envelope.loader;
}

View File

@@ -0,0 +1,217 @@
package net.woggioni.envelope.loader;
import lombok.SneakyThrows;
import net.woggioni.envelope.loader.JarFile;
import net.woggioni.envelope.loader.Handler;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReader;
import java.lang.module.ModuleReference;
import java.net.URI;
import java.net.URL;
import java.net.URLStreamHandler;
import java.nio.file.Paths;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.jar.Attributes.Name;
import java.util.jar.JarEntry;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class JarFileModuleFinder implements ModuleFinder {
private static final String MODULE_DESCRIPTOR_ENTRY = "module-info.class";
private static final Name AUTOMATIC_MODULE_NAME_MANIFEST_ENTRY = new Name("Automatic-Module-Name");
private static class Patterns {
static final Pattern DASH_VERSION = Pattern.compile("-(\\d+(\\.|$))");
static final Pattern NON_ALPHANUM = Pattern.compile("[^A-Za-z0-9]");
static final Pattern REPEATING_DOTS = Pattern.compile("(\\.)(\\1)+");
static final Pattern LEADING_DOTS = Pattern.compile("^\\.");
static final Pattern TRAILING_DOTS = Pattern.compile("\\.$");
}
private final Map<String, Map.Entry<ModuleReference, Handler>> modules;
@SneakyThrows
private static final URI toURI(URL url) {
return url.toURI();
}
public final URLStreamHandler getStreamHandlerForModule(String moduleName) {
return modules.get(moduleName).getValue();
}
private static String cleanModuleName(String mn) {
// replace non-alphanumeric
mn = Patterns.NON_ALPHANUM.matcher(mn).replaceAll(".");
// collapse repeating dots
mn = Patterns.REPEATING_DOTS.matcher(mn).replaceAll(".");
// drop leading dots
if (!mn.isEmpty() && mn.charAt(0) == '.')
mn = Patterns.LEADING_DOTS.matcher(mn).replaceAll("");
// drop trailing dots
int len = mn.length();
if (len > 0 && mn.charAt(len-1) == '.')
mn = Patterns.TRAILING_DOTS.matcher(mn).replaceAll("");
return mn;
}
@SneakyThrows
private static String moduleNameFromURI(URI uri) {
String fn = null;
URI tmp = uri;
while(true) {
String schemeSpecificPart = tmp.getSchemeSpecificPart();
if(tmp.getPath() != null) {
String path = tmp.getPath();
int end = path.lastIndexOf("!");
if(end == -1) end = path.length();
int start = path.lastIndexOf("!", end - 1);
if(start == -1) start = 0;
fn = Paths.get(path.substring(start, end)).getFileName().toString();
break;
} else {
tmp = new URI(schemeSpecificPart);
}
}
// Derive the version, and the module name if needed, from JAR file name
int i = fn.lastIndexOf(File.separator);
if (i != -1)
fn = fn.substring(i + 1);
// drop ".jar"
String name = fn.substring(0, fn.length() - 4);
String vs = null;
// find first occurrence of -${NUMBER}. or -${NUMBER}$
Matcher matcher = Patterns.DASH_VERSION.matcher(name);
if (matcher.find()) {
int start = matcher.start();
// attempt to parse the tail as a version string
try {
String tail = name.substring(start + 1);
ModuleDescriptor.Version.parse(tail);
vs = tail;
} catch (IllegalArgumentException ignore) { }
name = name.substring(0, start);
}
return cleanModuleName(name);
}
public JarFileModuleFinder(JarFile ...jarFiles) {
this(Arrays.asList(jarFiles));
}
private static final String META_INF_VERSION_PREFIX = "META-INF/versions/";
private static Set<String> collectPackageNames(JarFile jarFile) {
Set<String> result = jarFile
.stream()
.filter(entry -> entry.getName().endsWith(".class"))
.map(entry -> {
String entryName = entry.getName();
int lastSlash = entryName.lastIndexOf('/');
if(lastSlash < 0) return null;
else return entryName.substring(0, lastSlash).replace('/', '.');
})
.filter(Objects::nonNull)
.collect(Collectors.toSet());
return Collections.unmodifiableSet(result);
}
@SneakyThrows
public JarFileModuleFinder(Iterable<JarFile> jarFiles) {
TreeMap<String, Map.Entry<ModuleReference, Handler>> modules = new TreeMap<>();
for(JarFile jarFile : jarFiles) {
URI uri = jarFile.getUrl().toURI();
String moduleName = null;
ModuleDescriptor moduleDescriptor;
JarEntry moduleDescriptorEntry = jarFile.getJarEntry(MODULE_DESCRIPTOR_ENTRY);
if (moduleDescriptorEntry != null) {
try(InputStream is = jarFile.getInputStream(moduleDescriptorEntry)) {
moduleDescriptor = ModuleDescriptor.read(is, () -> collectPackageNames(jarFile));
}
} else {
Manifest mf = jarFile.getManifest();
moduleName = mf.getMainAttributes().getValue(AUTOMATIC_MODULE_NAME_MANIFEST_ENTRY);
if(moduleName == null) {
moduleName = moduleNameFromURI(uri);
}
ModuleDescriptor.Builder mdb = ModuleDescriptor.newAutomaticModule(moduleName);
mdb.packages(collectPackageNames(jarFile));
// Main-Class attribute if it exists
String mainClass = mf.getMainAttributes().getValue(Name.MAIN_CLASS);
if (mainClass != null) {
mdb.mainClass(mainClass);
}
moduleDescriptor = mdb.build();
}
modules.put(moduleDescriptor.name(),
new AbstractMap.SimpleEntry<>(new ModuleReference(moduleDescriptor, uri) {
@Override
public ModuleReader open() throws IOException {
return new ModuleReader() {
@Override
public Optional<URI> find(String name) throws IOException {
JarEntry jarEntry = jarFile.getJarEntry(name);
if(jarEntry == null) return Optional.empty();
return Optional.of(uri.resolve('!' + name));
}
@Override
public Optional<InputStream> open(String name) throws IOException {
JarEntry jarEntry = jarFile.getJarEntry(name);
if(jarEntry == null) return Optional.empty();
return Optional.of(jarFile.getInputStream(jarEntry));
}
@Override
public Stream<String> list() throws IOException {
return jarFile.stream().map(JarEntry::getName);
}
@Override
public void close() throws IOException {}
};
}
}, new Handler(jarFile)));
}
this.modules = Collections.unmodifiableMap(modules);
}
@Override
public Optional<ModuleReference> find(String name) {
return Optional.ofNullable(modules.get(name)).map(Map.Entry::getKey);
}
@Override
public Set<ModuleReference> findAll() {
return Collections.unmodifiableSet(modules.values()
.stream().map(Map.Entry::getKey)
.collect(Collectors.toSet()));
}
}

View File

@@ -0,0 +1,158 @@
package net.woggioni.envelope.loader;
import java.lang.module.ModuleReader;
import java.lang.module.ModuleReference;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
import java.util.function.Function;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import java.net.URI;
import java.net.URL;
import java.net.URLStreamHandler;
import java.nio.ByteBuffer;
import java.lang.module.ResolvedModule;
import java.lang.module.Configuration;
import java.security.CodeSource;
import java.security.SecureClassLoader;
import java.security.CodeSigner;
@RequiredArgsConstructor
public final class ModuleClassLoader extends SecureClassLoader {
static {
registerAsParallelCapable();
}
private static String className2ResourceName(String className) {
return className.replace('.', '/') + ".class";
}
private static String packageName(String cn) {
int pos = cn.lastIndexOf('.');
return (pos < 0) ? "" : cn.substring(0, pos);
}
private final Map<String, ClassLoader> packageMap;
private final ModuleReference moduleReference;
private final Function<URI, URL> urlConverter;
@Override
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(className)) {
Class<?> result = findLoadedClass(className);
if (result == null) {
result = findClass(className);
if(result == null) {
ClassLoader classLoader = packageMap.get(packageName(className));
if (classLoader != null && classLoader != this) {
result = classLoader.loadClass(className);
}
if(result == null) {
result = super.loadClass(className, resolve);
}
} else if(resolve) {
resolveClass(result);
}
}
return result;
}
}
@Override
protected URL findResource(String moduleName, String name) throws IOException {
if (Objects.equals(moduleReference.descriptor().name(), moduleName)) {
return findResource(name);
} else {
return null;
}
}
@Override
@SneakyThrows
protected URL findResource(String resource) {
try(ModuleReader reader = moduleReference.open()) {
Optional<ByteBuffer> byteBufferOptional = reader.read(resource);
if (byteBufferOptional.isPresent()) {
ByteBuffer byteBuffer = byteBufferOptional.get();
try {
return moduleReference.location()
.map(new Function<URI, URI>() {
@SneakyThrows
public URI apply(URI uri) {
return new URI(uri.toString() + resource);
}
}).map(urlConverter).orElse(null);
} finally {
reader.release(byteBuffer);
}
} else {
return null;
}
}
}
@Override
protected Enumeration<URL> findResources(final String resource) throws IOException {
return new Enumeration<URL>() {
private URL url = findResource(resource);
public boolean hasMoreElements() {
return url != null;
}
public URL nextElement() {
URL result = url;
url = null;
return result;
}
};
}
@Override
@SneakyThrows
protected Class<?> findClass(String className) throws ClassNotFoundException {
Class<?> result = findClass(moduleReference.descriptor().name(), className);
return result;
}
@Override
@SneakyThrows
protected Class<?> findClass(String moduleName, String className) {
if (Objects.equals(moduleReference.descriptor().name(), moduleName)) {
String resource = className.replace('.', '/').concat(".class");
Optional<ByteBuffer> byteBufferOptional;
try(ModuleReader reader = moduleReference.open()) {
byteBufferOptional = reader.read(resource);
if (byteBufferOptional.isPresent()) {
ByteBuffer byteBuffer = byteBufferOptional.get();
try {
URL location = moduleReference
.location()
.map(urlConverter)
.orElse(null);
CodeSource codeSource = new CodeSource(location, (CodeSigner[]) null);
return defineClass(className, byteBuffer, codeSource);
} finally {
reader.release(byteBuffer);
}
} else {
return null;
}
}
} else {
return null;
}
}
}

View File

@@ -24,3 +24,4 @@ rootProject.name = 'envelope'
include 'common'
include 'launcher'
include 'loader'

View File

@@ -24,9 +24,12 @@ import javax.annotation.Nonnull;
import javax.inject.Inject;
import java.io.File;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
@@ -119,11 +122,19 @@ public class EnvelopeJarTask extends AbstractArchiveTask {
private final ZipEntryFactory zipEntryFactory;
private final byte[] buffer;
private final List<String> libraries;
private static final String LIBRARY_PREFIX = Constants.LIBRARIES_FOLDER + '/';
@Override
@SneakyThrows
public void processFile(FileCopyDetailsInternal fileCopyDetails) {
String entryName = fileCopyDetails.getRelativePath().toString();
if (!fileCopyDetails.isDirectory() && entryName.startsWith(Constants.LIBRARIES_FOLDER)) {
int start = LIBRARY_PREFIX.length() + 1;
if (!fileCopyDetails.isDirectory() &&
entryName.startsWith(LIBRARY_PREFIX) &&
entryName.indexOf('/', start) < 0) {
libraries.add(entryName.substring(LIBRARY_PREFIX.length()));
Supplier<InputStream> streamSupplier = () -> Common.read(fileCopyDetails.getFile(), false);
Attributes attr = manifest.getEntries().computeIfAbsent(entryName, it -> new Attributes());
md.reset();
@@ -231,6 +242,7 @@ public class EnvelopeJarTask extends AbstractArchiveTask {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] buffer = new byte[Constants.BUFFER_SIZE];
List<String> libraries = new ArrayList<>();
/**
* The manifest has to be the first zip entry in a jar archive, as an example,
@@ -247,7 +259,8 @@ public class EnvelopeJarTask extends AbstractArchiveTask {
File temporaryJar = new File(getTemporaryDir(), "premature.zip");
try (ZipOutputStream zipOutputStream = new ZipOutputStream(Common.write(temporaryJar, true))) {
zipOutputStream.setLevel(NO_COMPRESSION);
StreamAction streamAction = new StreamAction(zipOutputStream, manifest, md, zipEntryFactory, buffer);
StreamAction streamAction = new StreamAction(
zipOutputStream, manifest, md, zipEntryFactory, buffer, libraries);
copyActionProcessingStream.process(streamAction);
}
@@ -272,6 +285,15 @@ public class EnvelopeJarTask extends AbstractArchiveTask {
props.setProperty(entry.getKey(), entry.getValue());
}
props.store(zipOutputStream, null);
zipEntry = zipEntryFactory.createZipEntry(Constants.LIBRARIES_TOC);
zipEntry.setMethod(ZipEntry.DEFLATED);
zipOutputStream.putNextEntry(zipEntry);
int i = 0;
while(i < libraries.size()) {
if(i > 0) zipOutputStream.write('/');
zipOutputStream.write(libraries.get(i).getBytes(StandardCharsets.UTF_8));
++i;
}
while (true) {
zipEntry = zipInputStream.getNextEntry();

View File

@@ -9,9 +9,6 @@ import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.JavaExec;
import org.gradle.api.tasks.bundling.Jar;
import org.gradle.process.CommandLineArgumentProvider;
import java.util.Arrays;
public class EnvelopePlugin implements Plugin<Project> {
@Override