added library toc file
This commit is contained in:
@@ -11,12 +11,6 @@ allprojects {
|
||||
apply plugin: 'net.woggioni.gradle.lombok'
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url = woggioniMavenRepositoryUrl
|
||||
content {
|
||||
includeModule 'net.woggioni', 'xclassloader'
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
|
@@ -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";
|
||||
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
@@ -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);
|
||||
|
||||
}
|
||||
|
@@ -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));
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
@@ -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
7
loader/build.gradle
Normal file
@@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
id 'net.woggioni.gradle.multi-release-jar'
|
||||
}
|
||||
|
||||
ext {
|
||||
setProperty('jpms.module.name', 'net.woggioni.envelope.loader')
|
||||
}
|
@@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
415
loader/src/main/java/net/woggioni/envelope/loader/Bytes.java
Normal file
415
loader/src/main/java/net/woggioni/envelope/loader/Bytes.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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();
|
||||
|
||||
}
|
@@ -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();
|
||||
|
||||
}
|
466
loader/src/main/java/net/woggioni/envelope/loader/Handler.java
Normal file
466
loader/src/main/java/net/woggioni/envelope/loader/Handler.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
120
loader/src/main/java/net/woggioni/envelope/loader/JarEntry.java
Normal file
120
loader/src/main/java/net/woggioni/envelope/loader/JarEntry.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
|
||||
}
|
490
loader/src/main/java/net/woggioni/envelope/loader/JarFile.java
Normal file
490
loader/src/main/java/net/woggioni/envelope/loader/JarFile.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -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];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -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");
|
||||
}
|
||||
|
||||
}
|
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -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();
|
||||
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Support for loading and manipulating JAR/WAR files.
|
||||
*/
|
||||
package net.woggioni.envelope.loader;
|
5
loader/src/main/java11/module-info.java
Normal file
5
loader/src/main/java11/module-info.java
Normal file
@@ -0,0 +1,5 @@
|
||||
module net.woggioni.envelope.loader {
|
||||
requires java.logging;
|
||||
requires static lombok;
|
||||
exports net.woggioni.envelope.loader;
|
||||
}
|
@@ -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()));
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -24,3 +24,4 @@ rootProject.name = 'envelope'
|
||||
|
||||
include 'common'
|
||||
include 'launcher'
|
||||
include 'loader'
|
||||
|
@@ -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();
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user