Rewrite nested jar support code and remove Java 8 support
Rewrite nested jar code to better align with the implementations
provided in Java 17. This update makes two fundamental changes to
the previous implementation:
- Resource cleanup is now handled using the `java.lang.ref.Cleaner`
- Jar URLs now use the form `jar:nested:/my.jar/!nested.jar!/entry`
Unlike the previous `jar🫙/my,jar!/nested.jar!/entry` URL format,
the new format is compatible with Java's default Jar URL handler.
Specifically, it now only uses a single `jar:` prefix and it no longer
includes multiple `!/` separators.
In addition to the changes above, many of the ancillary classes have
also been refactored and updated to create cleaner APIs.
Closes gh-37668
pull/37640/head
parent
75ddb9fa47
commit
7ad4a9817d
@ -1,207 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.springframework.boot.loader.archive.Archive;
|
||||
import org.springframework.boot.loader.archive.ExplodedArchive;
|
||||
|
||||
/**
|
||||
* Base class for executable archive {@link Launcher}s.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Madhura Bhave
|
||||
* @author Scott Frederick
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public abstract class ExecutableArchiveLauncher extends Launcher {
|
||||
|
||||
private static final String START_CLASS_ATTRIBUTE = "Start-Class";
|
||||
|
||||
protected static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index";
|
||||
|
||||
protected static final String DEFAULT_CLASSPATH_INDEX_FILE_NAME = "classpath.idx";
|
||||
|
||||
private final Archive archive;
|
||||
|
||||
private final ClassPathIndexFile classPathIndex;
|
||||
|
||||
public ExecutableArchiveLauncher() {
|
||||
try {
|
||||
this.archive = createArchive();
|
||||
this.classPathIndex = getClassPathIndex(this.archive);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected ExecutableArchiveLauncher(Archive archive) {
|
||||
try {
|
||||
this.archive = archive;
|
||||
this.classPathIndex = getClassPathIndex(this.archive);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
|
||||
// Only needed for exploded archives, regular ones already have a defined order
|
||||
if (archive instanceof ExplodedArchive) {
|
||||
String location = getClassPathIndexFileLocation(archive);
|
||||
return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getClassPathIndexFileLocation(Archive archive) throws IOException {
|
||||
Manifest manifest = archive.getManifest();
|
||||
Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null;
|
||||
String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null;
|
||||
return (location != null) ? location : getArchiveEntryPathPrefix() + DEFAULT_CLASSPATH_INDEX_FILE_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getMainClass() throws Exception {
|
||||
Manifest manifest = this.archive.getManifest();
|
||||
String mainClass = null;
|
||||
if (manifest != null) {
|
||||
mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE);
|
||||
}
|
||||
if (mainClass == null) {
|
||||
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
|
||||
}
|
||||
return mainClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
|
||||
List<URL> urls = new ArrayList<>(guessClassPathSize());
|
||||
while (archives.hasNext()) {
|
||||
urls.add(archives.next().getUrl());
|
||||
}
|
||||
if (this.classPathIndex != null) {
|
||||
urls.addAll(this.classPathIndex.getUrls());
|
||||
}
|
||||
return createClassLoader(urls.toArray(new URL[0]));
|
||||
}
|
||||
|
||||
private int guessClassPathSize() {
|
||||
if (this.classPathIndex != null) {
|
||||
return this.classPathIndex.size() + 10;
|
||||
}
|
||||
return 50;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
|
||||
Archive.EntryFilter searchFilter = this::isSearchCandidate;
|
||||
Iterator<Archive> archives = this.archive.getNestedArchives(searchFilter,
|
||||
(entry) -> isNestedArchive(entry) && !isEntryIndexed(entry));
|
||||
if (isPostProcessingClassPathArchives()) {
|
||||
archives = applyClassPathArchivePostProcessing(archives);
|
||||
}
|
||||
return archives;
|
||||
}
|
||||
|
||||
private boolean isEntryIndexed(Archive.Entry entry) {
|
||||
if (this.classPathIndex != null) {
|
||||
return this.classPathIndex.containsEntry(entry.getName());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Iterator<Archive> applyClassPathArchivePostProcessing(Iterator<Archive> archives) throws Exception {
|
||||
List<Archive> list = new ArrayList<>();
|
||||
while (archives.hasNext()) {
|
||||
list.add(archives.next());
|
||||
}
|
||||
postProcessClassPathArchives(list);
|
||||
return list.iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the specified entry is a candidate for further searching.
|
||||
* @param entry the entry to check
|
||||
* @return {@code true} if the entry is a candidate for further searching
|
||||
* @since 2.3.0
|
||||
*/
|
||||
protected boolean isSearchCandidate(Archive.Entry entry) {
|
||||
if (getArchiveEntryPathPrefix() == null) {
|
||||
return true;
|
||||
}
|
||||
return entry.getName().startsWith(getArchiveEntryPathPrefix());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the specified entry is a nested item that should be added to the
|
||||
* classpath.
|
||||
* @param entry the entry to check
|
||||
* @return {@code true} if the entry is a nested item (jar or directory)
|
||||
*/
|
||||
protected abstract boolean isNestedArchive(Archive.Entry entry);
|
||||
|
||||
/**
|
||||
* Return if post-processing needs to be applied to the archives. For back
|
||||
* compatibility this method returns {@code true}, but subclasses that don't override
|
||||
* {@link #postProcessClassPathArchives(List)} should provide an implementation that
|
||||
* returns {@code false}.
|
||||
* @return if the {@link #postProcessClassPathArchives(List)} method is implemented
|
||||
* @since 2.3.0
|
||||
*/
|
||||
protected boolean isPostProcessingClassPathArchives() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to post-process archive entries before they are used. Implementations can
|
||||
* add and remove entries.
|
||||
* @param archives the archives
|
||||
* @throws Exception if the post-processing fails
|
||||
* @see #isPostProcessingClassPathArchives()
|
||||
*/
|
||||
protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the path prefix for entries in the archive.
|
||||
* @return the path prefix
|
||||
*/
|
||||
protected String getArchiveEntryPathPrefix() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isExploded() {
|
||||
return this.archive.isExploded();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final Archive getArchive() {
|
||||
return this.archive;
|
||||
}
|
||||
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader;
|
||||
|
||||
import org.springframework.boot.loader.archive.Archive;
|
||||
import org.springframework.boot.loader.archive.Archive.EntryFilter;
|
||||
|
||||
/**
|
||||
* {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are
|
||||
* included inside a {@code /BOOT-INF/lib} directory and that application classes are
|
||||
* included inside a {@code /BOOT-INF/classes} directory.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Madhura Bhave
|
||||
* @author Scott Frederick
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class JarLauncher extends ExecutableArchiveLauncher {
|
||||
|
||||
static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
|
||||
if (entry.isDirectory()) {
|
||||
return entry.getName().equals("BOOT-INF/classes/");
|
||||
}
|
||||
return entry.getName().startsWith("BOOT-INF/lib/");
|
||||
};
|
||||
|
||||
public JarLauncher() {
|
||||
}
|
||||
|
||||
protected JarLauncher(Archive archive) {
|
||||
super(archive);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isPostProcessingClassPathArchives() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isNestedArchive(Archive.Entry entry) {
|
||||
return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getArchiveEntryPathPrefix() {
|
||||
return "BOOT-INF/";
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
new JarLauncher().launch(args);
|
||||
}
|
||||
|
||||
}
|
@ -1,366 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.JarURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.net.URLConnection;
|
||||
import java.util.Enumeration;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.springframework.boot.loader.archive.Archive;
|
||||
import org.springframework.boot.loader.jar.Handler;
|
||||
|
||||
/**
|
||||
* {@link ClassLoader} used by the {@link Launcher}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @author Andy Wilkinson
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class LaunchedURLClassLoader extends URLClassLoader {
|
||||
|
||||
private static final int BUFFER_SIZE = 4096;
|
||||
|
||||
static {
|
||||
ClassLoader.registerAsParallelCapable();
|
||||
}
|
||||
|
||||
private final boolean exploded;
|
||||
|
||||
private final Archive rootArchive;
|
||||
|
||||
private final Object packageLock = new Object();
|
||||
|
||||
private volatile DefinePackageCallType definePackageCallType;
|
||||
|
||||
/**
|
||||
* Create a new {@link LaunchedURLClassLoader} instance.
|
||||
* @param urls the URLs from which to load classes and resources
|
||||
* @param parent the parent class loader for delegation
|
||||
*/
|
||||
public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
|
||||
this(false, urls, parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link LaunchedURLClassLoader} instance.
|
||||
* @param exploded if the underlying archive is exploded
|
||||
* @param urls the URLs from which to load classes and resources
|
||||
* @param parent the parent class loader for delegation
|
||||
*/
|
||||
public LaunchedURLClassLoader(boolean exploded, URL[] urls, ClassLoader parent) {
|
||||
this(exploded, null, urls, parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link LaunchedURLClassLoader} instance.
|
||||
* @param exploded if the underlying archive is exploded
|
||||
* @param rootArchive the root archive or {@code null}
|
||||
* @param urls the URLs from which to load classes and resources
|
||||
* @param parent the parent class loader for delegation
|
||||
* @since 2.3.1
|
||||
*/
|
||||
public LaunchedURLClassLoader(boolean exploded, Archive rootArchive, URL[] urls, ClassLoader parent) {
|
||||
super(urls, parent);
|
||||
this.exploded = exploded;
|
||||
this.rootArchive = rootArchive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL findResource(String name) {
|
||||
if (this.exploded) {
|
||||
return super.findResource(name);
|
||||
}
|
||||
Handler.setUseFastConnectionExceptions(true);
|
||||
try {
|
||||
return super.findResource(name);
|
||||
}
|
||||
finally {
|
||||
Handler.setUseFastConnectionExceptions(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<URL> findResources(String name) throws IOException {
|
||||
if (this.exploded) {
|
||||
return super.findResources(name);
|
||||
}
|
||||
Handler.setUseFastConnectionExceptions(true);
|
||||
try {
|
||||
return new UseFastConnectionExceptionsEnumeration(super.findResources(name));
|
||||
}
|
||||
finally {
|
||||
Handler.setUseFastConnectionExceptions(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
if (name.startsWith("org.springframework.boot.loader.jarmode.")) {
|
||||
try {
|
||||
Class<?> result = loadClassInLaunchedClassLoader(name);
|
||||
if (resolve) {
|
||||
resolveClass(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
}
|
||||
}
|
||||
if (this.exploded) {
|
||||
return super.loadClass(name, resolve);
|
||||
}
|
||||
Handler.setUseFastConnectionExceptions(true);
|
||||
try {
|
||||
try {
|
||||
definePackageIfNecessary(name);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// Tolerate race condition due to being parallel capable
|
||||
if (getDefinedPackage(name) == null) {
|
||||
// This should never happen as the IllegalArgumentException indicates
|
||||
// that the package has already been defined and, therefore,
|
||||
// getDefinedPackage(name) should not return null.
|
||||
throw new AssertionError("Package " + name + " has already been defined but it could not be found");
|
||||
}
|
||||
}
|
||||
return super.loadClass(name, resolve);
|
||||
}
|
||||
finally {
|
||||
Handler.setUseFastConnectionExceptions(false);
|
||||
}
|
||||
}
|
||||
|
||||
private Class<?> loadClassInLaunchedClassLoader(String name) throws ClassNotFoundException {
|
||||
String internalName = name.replace('.', '/') + ".class";
|
||||
InputStream inputStream = getParent().getResourceAsStream(internalName);
|
||||
if (inputStream == null) {
|
||||
throw new ClassNotFoundException(name);
|
||||
}
|
||||
try {
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int bytesRead = -1;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
inputStream.close();
|
||||
byte[] bytes = outputStream.toByteArray();
|
||||
Class<?> definedClass = defineClass(name, bytes, 0, bytes.length);
|
||||
definePackageIfNecessary(name);
|
||||
return definedClass;
|
||||
}
|
||||
finally {
|
||||
inputStream.close();
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a package before a {@code findClass} call is made. This is necessary to
|
||||
* ensure that the appropriate manifest for nested JARs is associated with the
|
||||
* package.
|
||||
* @param className the class name being found
|
||||
*/
|
||||
private void definePackageIfNecessary(String className) {
|
||||
int lastDot = className.lastIndexOf('.');
|
||||
if (lastDot >= 0) {
|
||||
String packageName = className.substring(0, lastDot);
|
||||
if (getDefinedPackage(packageName) == null) {
|
||||
try {
|
||||
definePackage(className, packageName);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// Tolerate race condition due to being parallel capable
|
||||
if (getDefinedPackage(packageName) == null) {
|
||||
// This should never happen as the IllegalArgumentException
|
||||
// indicates that the package has already been defined and,
|
||||
// therefore, getDefinedPackage(name) should not have returned
|
||||
// null.
|
||||
throw new AssertionError(
|
||||
"Package " + packageName + " has already been defined but it could not be found");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void definePackage(String className, String packageName) {
|
||||
String packageEntryName = packageName.replace('.', '/') + "/";
|
||||
String classEntryName = className.replace('.', '/') + ".class";
|
||||
for (URL url : getURLs()) {
|
||||
try {
|
||||
URLConnection connection = url.openConnection();
|
||||
if (connection instanceof JarURLConnection jarURLConnection) {
|
||||
JarFile jarFile = jarURLConnection.getJarFile();
|
||||
if (jarFile.getEntry(classEntryName) != null && jarFile.getEntry(packageEntryName) != null
|
||||
&& jarFile.getManifest() != null) {
|
||||
definePackage(packageName, jarFile.getManifest(), url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Package definePackage(String name, Manifest man, URL url) throws IllegalArgumentException {
|
||||
if (!this.exploded) {
|
||||
return super.definePackage(name, man, url);
|
||||
}
|
||||
synchronized (this.packageLock) {
|
||||
return doDefinePackage(DefinePackageCallType.MANIFEST, () -> super.definePackage(name, man, url));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Package definePackage(String name, String specTitle, String specVersion, String specVendor,
|
||||
String implTitle, String implVersion, String implVendor, URL sealBase) throws IllegalArgumentException {
|
||||
if (!this.exploded) {
|
||||
return super.definePackage(name, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor,
|
||||
sealBase);
|
||||
}
|
||||
synchronized (this.packageLock) {
|
||||
if (this.definePackageCallType == null) {
|
||||
// We're not part of a call chain which means that the URLClassLoader
|
||||
// is trying to define a package for our exploded JAR. We use the
|
||||
// manifest version to ensure package attributes are set
|
||||
Manifest manifest = getManifest(this.rootArchive);
|
||||
if (manifest != null) {
|
||||
return definePackage(name, manifest, sealBase);
|
||||
}
|
||||
}
|
||||
return doDefinePackage(DefinePackageCallType.ATTRIBUTES, () -> super.definePackage(name, specTitle,
|
||||
specVersion, specVendor, implTitle, implVersion, implVendor, sealBase));
|
||||
}
|
||||
}
|
||||
|
||||
private Manifest getManifest(Archive archive) {
|
||||
try {
|
||||
return (archive != null) ? archive.getManifest() : null;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T doDefinePackage(DefinePackageCallType type, Supplier<T> call) {
|
||||
DefinePackageCallType existingType = this.definePackageCallType;
|
||||
try {
|
||||
this.definePackageCallType = type;
|
||||
return call.get();
|
||||
}
|
||||
finally {
|
||||
this.definePackageCallType = existingType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear URL caches.
|
||||
*/
|
||||
public void clearCache() {
|
||||
if (this.exploded) {
|
||||
return;
|
||||
}
|
||||
for (URL url : getURLs()) {
|
||||
try {
|
||||
URLConnection connection = url.openConnection();
|
||||
if (connection instanceof JarURLConnection) {
|
||||
clearCache(connection);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void clearCache(URLConnection connection) throws IOException {
|
||||
Object jarFile = ((JarURLConnection) connection).getJarFile();
|
||||
if (jarFile instanceof org.springframework.boot.loader.jar.JarFile) {
|
||||
((org.springframework.boot.loader.jar.JarFile) jarFile).clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
private static class UseFastConnectionExceptionsEnumeration implements Enumeration<URL> {
|
||||
|
||||
private final Enumeration<URL> delegate;
|
||||
|
||||
UseFastConnectionExceptionsEnumeration(Enumeration<URL> delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMoreElements() {
|
||||
Handler.setUseFastConnectionExceptions(true);
|
||||
try {
|
||||
return this.delegate.hasMoreElements();
|
||||
}
|
||||
finally {
|
||||
Handler.setUseFastConnectionExceptions(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL nextElement() {
|
||||
Handler.setUseFastConnectionExceptions(true);
|
||||
try {
|
||||
return this.delegate.nextElement();
|
||||
}
|
||||
finally {
|
||||
Handler.setUseFastConnectionExceptions(false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The different types of call made to define a package. We track these for exploded
|
||||
* jars so that we can detect packages that should have manifest attributes applied.
|
||||
*/
|
||||
private enum DefinePackageCallType {
|
||||
|
||||
/**
|
||||
* A define package call from a resource that has a manifest.
|
||||
*/
|
||||
MANIFEST,
|
||||
|
||||
/**
|
||||
* A define package call with a direct set of attributes.
|
||||
*/
|
||||
ATTRIBUTES
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,159 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.security.CodeSource;
|
||||
import java.security.ProtectionDomain;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.loader.archive.Archive;
|
||||
import org.springframework.boot.loader.archive.ExplodedArchive;
|
||||
import org.springframework.boot.loader.archive.JarFileArchive;
|
||||
import org.springframework.boot.loader.jar.JarFile;
|
||||
|
||||
/**
|
||||
* Base class for launchers that can start an application with a fully configured
|
||||
* classpath backed by one or more {@link Archive}s.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public abstract class Launcher {
|
||||
|
||||
private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher";
|
||||
|
||||
/**
|
||||
* Launch the application. This method is the initial entry point that should be
|
||||
* called by a subclass {@code public static void main(String[] args)} method.
|
||||
* @param args the incoming arguments
|
||||
* @throws Exception if the application fails to launch
|
||||
*/
|
||||
protected void launch(String[] args) throws Exception {
|
||||
if (!isExploded()) {
|
||||
JarFile.registerUrlProtocolHandler();
|
||||
}
|
||||
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
|
||||
String jarMode = System.getProperty("jarmode");
|
||||
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
|
||||
launch(args, launchClass, classLoader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a classloader for the specified archives.
|
||||
* @param archives the archives
|
||||
* @return the classloader
|
||||
* @throws Exception if the classloader cannot be created
|
||||
* @since 2.3.0
|
||||
*/
|
||||
protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
|
||||
List<URL> urls = new ArrayList<>(50);
|
||||
while (archives.hasNext()) {
|
||||
urls.add(archives.next().getUrl());
|
||||
}
|
||||
return createClassLoader(urls.toArray(new URL[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a classloader for the specified URLs.
|
||||
* @param urls the URLs
|
||||
* @return the classloader
|
||||
* @throws Exception if the classloader cannot be created
|
||||
*/
|
||||
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
|
||||
return new LaunchedURLClassLoader(isExploded(), getArchive(), urls, getClass().getClassLoader());
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch the application given the archive file and a fully configured classloader.
|
||||
* @param args the incoming arguments
|
||||
* @param launchClass the launch class to run
|
||||
* @param classLoader the classloader
|
||||
* @throws Exception if the launch fails
|
||||
*/
|
||||
protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
|
||||
Thread.currentThread().setContextClassLoader(classLoader);
|
||||
createMainMethodRunner(launchClass, args, classLoader).run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the {@code MainMethodRunner} used to launch the application.
|
||||
* @param mainClass the main class
|
||||
* @param args the incoming arguments
|
||||
* @param classLoader the classloader
|
||||
* @return the main method runner
|
||||
*/
|
||||
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
|
||||
return new MainMethodRunner(mainClass, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the main class that should be launched.
|
||||
* @return the name of the main class
|
||||
* @throws Exception if the main class cannot be obtained
|
||||
*/
|
||||
protected abstract String getMainClass() throws Exception;
|
||||
|
||||
/**
|
||||
* Returns the archives that will be used to construct the class path.
|
||||
* @return the class path archives
|
||||
* @throws Exception if the class path archives cannot be obtained
|
||||
* @since 2.3.0
|
||||
*/
|
||||
protected abstract Iterator<Archive> getClassPathArchivesIterator() throws Exception;
|
||||
|
||||
protected final Archive createArchive() throws Exception {
|
||||
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
|
||||
CodeSource codeSource = protectionDomain.getCodeSource();
|
||||
URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
|
||||
String path = (location != null) ? location.getSchemeSpecificPart() : null;
|
||||
if (path == null) {
|
||||
throw new IllegalStateException("Unable to determine code source archive");
|
||||
}
|
||||
File root = new File(path);
|
||||
if (!root.exists()) {
|
||||
throw new IllegalStateException("Unable to determine code source archive from " + root);
|
||||
}
|
||||
return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the launcher is running in an exploded mode. If this method returns
|
||||
* {@code true} then only regular JARs are supported and the additional URL and
|
||||
* ClassLoader support infrastructure can be optimized.
|
||||
* @return if the jar is exploded.
|
||||
* @since 2.3.0
|
||||
*/
|
||||
protected boolean isExploded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the root archive.
|
||||
* @return the root archive
|
||||
* @since 2.3.1
|
||||
*/
|
||||
protected Archive getArchive() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Utility class that is used by {@link Launcher}s to call a main method. The class
|
||||
* containing the main method is loaded using the thread context class loader.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class MainMethodRunner {
|
||||
|
||||
private final String mainClassName;
|
||||
|
||||
private final String[] args;
|
||||
|
||||
/**
|
||||
* Create a new {@link MainMethodRunner} instance.
|
||||
* @param mainClass the main class
|
||||
* @param args incoming arguments
|
||||
*/
|
||||
public MainMethodRunner(String mainClass, String[] args) {
|
||||
this.mainClassName = mainClass;
|
||||
this.args = (args != null) ? args.clone() : null;
|
||||
}
|
||||
|
||||
public void run() throws Exception {
|
||||
Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
|
||||
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
|
||||
mainMethod.setAccessible(true);
|
||||
mainMethod.invoke(null, new Object[] { this.args });
|
||||
}
|
||||
|
||||
}
|
@ -1,726 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.boot.loader.archive.Archive;
|
||||
import org.springframework.boot.loader.archive.Archive.Entry;
|
||||
import org.springframework.boot.loader.archive.Archive.EntryFilter;
|
||||
import org.springframework.boot.loader.archive.ExplodedArchive;
|
||||
import org.springframework.boot.loader.archive.JarFileArchive;
|
||||
import org.springframework.boot.loader.util.SystemPropertyUtils;
|
||||
|
||||
/**
|
||||
* {@link Launcher} for archives with user-configured classpath and main class through a
|
||||
* properties file. This model is often more flexible and more amenable to creating
|
||||
* well-behaved OS-level services than a model based on executable jars.
|
||||
* <p>
|
||||
* Looks in various places for a properties file to extract loader settings, defaulting to
|
||||
* {@code loader.properties} either on the current classpath or in the current working
|
||||
* directory. The name of the properties file can be changed by setting a System property
|
||||
* {@code loader.config.name} (e.g. {@code -Dloader.config.name=foo} will look for
|
||||
* {@code foo.properties}. If that file doesn't exist then tries
|
||||
* {@code loader.config.location} (with allowed prefixes {@code classpath:} and
|
||||
* {@code file:} or any valid URL). Once that file is located turns it into Properties and
|
||||
* extracts optional values (which can also be provided overridden as System properties in
|
||||
* case the file doesn't exist):
|
||||
* <ul>
|
||||
* <li>{@code loader.path}: a comma-separated list of directories (containing file
|
||||
* resources and/or nested archives in *.jar or *.zip or archives) or archives to append
|
||||
* to the classpath. {@code BOOT-INF/classes,BOOT-INF/lib} in the application archive are
|
||||
* always used</li>
|
||||
* <li>{@code loader.main}: the main method to delegate execution to once the class loader
|
||||
* is set up. No default, but will fall back to looking for a {@code Start-Class} in a
|
||||
* {@code MANIFEST.MF}, if there is one in <code>${loader.home}/META-INF</code>.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Janne Valkealahti
|
||||
* @author Andy Wilkinson
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class PropertiesLauncher extends Launcher {
|
||||
|
||||
private static final Class<?>[] PARENT_ONLY_PARAMS = new Class<?>[] { ClassLoader.class };
|
||||
|
||||
private static final Class<?>[] URLS_AND_PARENT_PARAMS = new Class<?>[] { URL[].class, ClassLoader.class };
|
||||
|
||||
private static final Class<?>[] NO_PARAMS = new Class<?>[] {};
|
||||
|
||||
private static final URL[] NO_URLS = new URL[0];
|
||||
|
||||
private static final String DEBUG = "loader.debug";
|
||||
|
||||
/**
|
||||
* Properties key for main class. As a manifest entry can also be specified as
|
||||
* {@code Start-Class}.
|
||||
*/
|
||||
public static final String MAIN = "loader.main";
|
||||
|
||||
/**
|
||||
* Properties key for classpath entries (directories possibly containing jars or
|
||||
* jars). Multiple entries can be specified using a comma-separated list. {@code
|
||||
* BOOT-INF/classes,BOOT-INF/lib} in the application archive are always used.
|
||||
*/
|
||||
public static final String PATH = "loader.path";
|
||||
|
||||
/**
|
||||
* Properties key for home directory. This is the location of external configuration
|
||||
* if not on classpath, and also the base path for any relative paths in the
|
||||
* {@link #PATH loader path}. Defaults to current working directory (
|
||||
* <code>${user.dir}</code>).
|
||||
*/
|
||||
public static final String HOME = "loader.home";
|
||||
|
||||
/**
|
||||
* Properties key for default command line arguments. These arguments (if present) are
|
||||
* prepended to the main method arguments before launching.
|
||||
*/
|
||||
public static final String ARGS = "loader.args";
|
||||
|
||||
/**
|
||||
* Properties key for name of external configuration file (excluding suffix). Defaults
|
||||
* to "application". Ignored if {@link #CONFIG_LOCATION loader config location} is
|
||||
* provided instead.
|
||||
*/
|
||||
public static final String CONFIG_NAME = "loader.config.name";
|
||||
|
||||
/**
|
||||
* Properties key for config file location (including optional classpath:, file: or
|
||||
* URL prefix).
|
||||
*/
|
||||
public static final String CONFIG_LOCATION = "loader.config.location";
|
||||
|
||||
/**
|
||||
* Properties key for boolean flag (default false) which, if set, will cause the
|
||||
* external configuration properties to be copied to System properties (assuming that
|
||||
* is allowed by Java security).
|
||||
*/
|
||||
public static final String SET_SYSTEM_PROPERTIES = "loader.system";
|
||||
|
||||
private static final Pattern WORD_SEPARATOR = Pattern.compile("\\W+");
|
||||
|
||||
private static final String NESTED_ARCHIVE_SEPARATOR = "!" + File.separator;
|
||||
|
||||
private final File home;
|
||||
|
||||
private List<String> paths = new ArrayList<>();
|
||||
|
||||
private final Properties properties = new Properties();
|
||||
|
||||
private final Archive parent;
|
||||
|
||||
private volatile ClassPathArchives classPathArchives;
|
||||
|
||||
public PropertiesLauncher() {
|
||||
try {
|
||||
this.home = getHomeDirectory();
|
||||
initializeProperties();
|
||||
initializePaths();
|
||||
this.parent = createArchive();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected File getHomeDirectory() {
|
||||
try {
|
||||
return new File(getPropertyWithDefault(HOME, "${user.dir}"));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeProperties() throws Exception {
|
||||
List<String> configs = new ArrayList<>();
|
||||
if (getProperty(CONFIG_LOCATION) != null) {
|
||||
configs.add(getProperty(CONFIG_LOCATION));
|
||||
}
|
||||
else {
|
||||
String[] names = getPropertyWithDefault(CONFIG_NAME, "loader").split(",");
|
||||
for (String name : names) {
|
||||
configs.add("file:" + getHomeDirectory() + "/" + name + ".properties");
|
||||
configs.add("classpath:" + name + ".properties");
|
||||
configs.add("classpath:BOOT-INF/classes/" + name + ".properties");
|
||||
}
|
||||
}
|
||||
for (String config : configs) {
|
||||
try (InputStream resource = getResource(config)) {
|
||||
if (resource != null) {
|
||||
debug("Found: " + config);
|
||||
loadResource(resource);
|
||||
// Load the first one we find
|
||||
return;
|
||||
}
|
||||
else {
|
||||
debug("Not found: " + config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadResource(InputStream resource) throws Exception {
|
||||
this.properties.load(resource);
|
||||
for (Object key : Collections.list(this.properties.propertyNames())) {
|
||||
String text = this.properties.getProperty((String) key);
|
||||
String value = SystemPropertyUtils.resolvePlaceholders(this.properties, text);
|
||||
if (value != null) {
|
||||
this.properties.put(key, value);
|
||||
}
|
||||
}
|
||||
if ("true".equals(getProperty(SET_SYSTEM_PROPERTIES))) {
|
||||
debug("Adding resolved properties to System properties");
|
||||
for (Object key : Collections.list(this.properties.propertyNames())) {
|
||||
String value = this.properties.getProperty((String) key);
|
||||
System.setProperty((String) key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream getResource(String config) throws Exception {
|
||||
if (config.startsWith("classpath:")) {
|
||||
return getClasspathResource(config.substring("classpath:".length()));
|
||||
}
|
||||
config = handleUrl(config);
|
||||
if (isUrl(config)) {
|
||||
return getURLResource(config);
|
||||
}
|
||||
return getFileResource(config);
|
||||
}
|
||||
|
||||
private String handleUrl(String path) throws UnsupportedEncodingException {
|
||||
if (path.startsWith("jar:file:") || path.startsWith("file:")) {
|
||||
path = URLDecoder.decode(path, "UTF-8");
|
||||
if (path.startsWith("file:")) {
|
||||
path = path.substring("file:".length());
|
||||
if (path.startsWith("//")) {
|
||||
path = path.substring(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private boolean isUrl(String config) {
|
||||
return config.contains("://");
|
||||
}
|
||||
|
||||
private InputStream getClasspathResource(String config) {
|
||||
while (config.startsWith("/")) {
|
||||
config = config.substring(1);
|
||||
}
|
||||
config = "/" + config;
|
||||
debug("Trying classpath: " + config);
|
||||
return getClass().getResourceAsStream(config);
|
||||
}
|
||||
|
||||
private InputStream getFileResource(String config) throws Exception {
|
||||
File file = new File(config);
|
||||
debug("Trying file: " + config);
|
||||
if (file.canRead()) {
|
||||
return new FileInputStream(file);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private InputStream getURLResource(String config) throws Exception {
|
||||
URL url = new URL(config);
|
||||
if (exists(url)) {
|
||||
URLConnection con = url.openConnection();
|
||||
try {
|
||||
return con.getInputStream();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// Close the HTTP connection (if applicable).
|
||||
if (con instanceof HttpURLConnection httpURLConnection) {
|
||||
httpURLConnection.disconnect();
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean exists(URL url) throws IOException {
|
||||
// Try a URL connection content-length header...
|
||||
URLConnection connection = url.openConnection();
|
||||
try {
|
||||
connection.setUseCaches(connection.getClass().getSimpleName().startsWith("JNLP"));
|
||||
if (connection instanceof HttpURLConnection httpConnection) {
|
||||
httpConnection.setRequestMethod("HEAD");
|
||||
int responseCode = httpConnection.getResponseCode();
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
return true;
|
||||
}
|
||||
else if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return (connection.getContentLength() >= 0);
|
||||
}
|
||||
finally {
|
||||
if (connection instanceof HttpURLConnection httpURLConnection) {
|
||||
httpURLConnection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initializePaths() throws Exception {
|
||||
String path = getProperty(PATH);
|
||||
if (path != null) {
|
||||
this.paths = parsePathsProperty(path);
|
||||
}
|
||||
debug("Nested archive paths: " + this.paths);
|
||||
}
|
||||
|
||||
private List<String> parsePathsProperty(String commaSeparatedPaths) {
|
||||
List<String> paths = new ArrayList<>();
|
||||
for (String path : commaSeparatedPaths.split(",")) {
|
||||
path = cleanupPath(path);
|
||||
// "" means the user wants root of archive but not current directory
|
||||
path = (path == null || path.isEmpty()) ? "/" : path;
|
||||
paths.add(path);
|
||||
}
|
||||
if (paths.isEmpty()) {
|
||||
paths.add("lib");
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
protected String[] getArgs(String... args) throws Exception {
|
||||
String loaderArgs = getProperty(ARGS);
|
||||
if (loaderArgs != null) {
|
||||
String[] defaultArgs = loaderArgs.split("\\s+");
|
||||
String[] additionalArgs = args;
|
||||
args = new String[defaultArgs.length + additionalArgs.length];
|
||||
System.arraycopy(defaultArgs, 0, args, 0, defaultArgs.length);
|
||||
System.arraycopy(additionalArgs, 0, args, defaultArgs.length, additionalArgs.length);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getMainClass() throws Exception {
|
||||
String mainClass = getProperty(MAIN, "Start-Class");
|
||||
if (mainClass == null) {
|
||||
throw new IllegalStateException("No '" + MAIN + "' or 'Start-Class' specified");
|
||||
}
|
||||
return mainClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
|
||||
String customLoaderClassName = getProperty("loader.classLoader");
|
||||
if (customLoaderClassName == null) {
|
||||
return super.createClassLoader(archives);
|
||||
}
|
||||
Set<URL> urls = new LinkedHashSet<>();
|
||||
while (archives.hasNext()) {
|
||||
urls.add(archives.next().getUrl());
|
||||
}
|
||||
ClassLoader loader = new LaunchedURLClassLoader(urls.toArray(NO_URLS), getClass().getClassLoader());
|
||||
debug("Classpath for custom loader: " + urls);
|
||||
loader = wrapWithCustomClassLoader(loader, customLoaderClassName);
|
||||
debug("Using custom class loader: " + customLoaderClassName);
|
||||
return loader;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ClassLoader wrapWithCustomClassLoader(ClassLoader parent, String className) throws Exception {
|
||||
Class<ClassLoader> type = (Class<ClassLoader>) Class.forName(className, true, parent);
|
||||
ClassLoader classLoader = newClassLoader(type, PARENT_ONLY_PARAMS, parent);
|
||||
if (classLoader == null) {
|
||||
classLoader = newClassLoader(type, URLS_AND_PARENT_PARAMS, NO_URLS, parent);
|
||||
}
|
||||
if (classLoader == null) {
|
||||
classLoader = newClassLoader(type, NO_PARAMS);
|
||||
}
|
||||
if (classLoader == null) {
|
||||
throw new IllegalArgumentException("Unable to create class loader for " + className);
|
||||
}
|
||||
return classLoader;
|
||||
}
|
||||
|
||||
private ClassLoader newClassLoader(Class<ClassLoader> loaderClass, Class<?>[] parameterTypes, Object... initargs)
|
||||
throws Exception {
|
||||
try {
|
||||
Constructor<ClassLoader> constructor = loaderClass.getDeclaredConstructor(parameterTypes);
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(initargs);
|
||||
}
|
||||
catch (NoSuchMethodException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getProperty(String propertyKey) throws Exception {
|
||||
return getProperty(propertyKey, null, null);
|
||||
}
|
||||
|
||||
private String getProperty(String propertyKey, String manifestKey) throws Exception {
|
||||
return getProperty(propertyKey, manifestKey, null);
|
||||
}
|
||||
|
||||
private String getPropertyWithDefault(String propertyKey, String defaultValue) throws Exception {
|
||||
return getProperty(propertyKey, null, defaultValue);
|
||||
}
|
||||
|
||||
private String getProperty(String propertyKey, String manifestKey, String defaultValue) throws Exception {
|
||||
if (manifestKey == null) {
|
||||
manifestKey = propertyKey.replace('.', '-');
|
||||
manifestKey = toCamelCase(manifestKey);
|
||||
}
|
||||
String property = SystemPropertyUtils.getProperty(propertyKey);
|
||||
if (property != null) {
|
||||
String value = SystemPropertyUtils.resolvePlaceholders(this.properties, property);
|
||||
debug("Property '" + propertyKey + "' from environment: " + value);
|
||||
return value;
|
||||
}
|
||||
if (this.properties.containsKey(propertyKey)) {
|
||||
String value = SystemPropertyUtils.resolvePlaceholders(this.properties,
|
||||
this.properties.getProperty(propertyKey));
|
||||
debug("Property '" + propertyKey + "' from properties: " + value);
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
if (this.home != null) {
|
||||
// Prefer home dir for MANIFEST if there is one
|
||||
try (ExplodedArchive archive = new ExplodedArchive(this.home, false)) {
|
||||
Manifest manifest = archive.getManifest();
|
||||
if (manifest != null) {
|
||||
String value = manifest.getMainAttributes().getValue(manifestKey);
|
||||
if (value != null) {
|
||||
debug("Property '" + manifestKey + "' from home directory manifest: " + value);
|
||||
return SystemPropertyUtils.resolvePlaceholders(this.properties, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IllegalStateException ex) {
|
||||
// Ignore
|
||||
}
|
||||
// Otherwise try the parent archive
|
||||
Manifest manifest = createArchive().getManifest();
|
||||
if (manifest != null) {
|
||||
String value = manifest.getMainAttributes().getValue(manifestKey);
|
||||
if (value != null) {
|
||||
debug("Property '" + manifestKey + "' from archive manifest: " + value);
|
||||
return SystemPropertyUtils.resolvePlaceholders(this.properties, value);
|
||||
}
|
||||
}
|
||||
return (defaultValue != null) ? SystemPropertyUtils.resolvePlaceholders(this.properties, defaultValue)
|
||||
: defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
|
||||
ClassPathArchives classPathArchives = this.classPathArchives;
|
||||
if (classPathArchives == null) {
|
||||
classPathArchives = new ClassPathArchives();
|
||||
this.classPathArchives = classPathArchives;
|
||||
}
|
||||
return classPathArchives.iterator();
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
PropertiesLauncher launcher = new PropertiesLauncher();
|
||||
args = launcher.getArgs(args);
|
||||
launcher.launch(args);
|
||||
}
|
||||
|
||||
public static String toCamelCase(CharSequence string) {
|
||||
if (string == null) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
Matcher matcher = WORD_SEPARATOR.matcher(string);
|
||||
int pos = 0;
|
||||
while (matcher.find()) {
|
||||
builder.append(capitalize(string.subSequence(pos, matcher.end()).toString()));
|
||||
pos = matcher.end();
|
||||
}
|
||||
builder.append(capitalize(string.subSequence(pos, string.length()).toString()));
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String capitalize(String str) {
|
||||
return Character.toUpperCase(str.charAt(0)) + str.substring(1);
|
||||
}
|
||||
|
||||
private void debug(String message) {
|
||||
if (Boolean.getBoolean(DEBUG)) {
|
||||
System.out.println(message);
|
||||
}
|
||||
}
|
||||
|
||||
private String cleanupPath(String path) {
|
||||
path = path.trim();
|
||||
// No need for current dir path
|
||||
if (path.startsWith("./")) {
|
||||
path = path.substring(2);
|
||||
}
|
||||
String lowerCasePath = path.toLowerCase(Locale.ENGLISH);
|
||||
if (lowerCasePath.endsWith(".jar") || lowerCasePath.endsWith(".zip")) {
|
||||
return path;
|
||||
}
|
||||
if (path.endsWith("/*")) {
|
||||
path = path.substring(0, path.length() - 1);
|
||||
}
|
||||
else {
|
||||
// It's a directory
|
||||
if (!path.endsWith("/") && !path.equals(".")) {
|
||||
path = path + "/";
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
void close() throws Exception {
|
||||
if (this.classPathArchives != null) {
|
||||
this.classPathArchives.close();
|
||||
}
|
||||
if (this.parent != null) {
|
||||
this.parent.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An iterable collection of the classpath archives.
|
||||
*/
|
||||
private class ClassPathArchives implements Iterable<Archive> {
|
||||
|
||||
private final List<Archive> classPathArchives;
|
||||
|
||||
private final List<JarFileArchive> jarFileArchives = new ArrayList<>();
|
||||
|
||||
ClassPathArchives() throws Exception {
|
||||
this.classPathArchives = new ArrayList<>();
|
||||
for (String path : PropertiesLauncher.this.paths) {
|
||||
for (Archive archive : getClassPathArchives(path)) {
|
||||
addClassPathArchive(archive);
|
||||
}
|
||||
}
|
||||
addNestedEntries();
|
||||
}
|
||||
|
||||
private void addClassPathArchive(Archive archive) throws IOException {
|
||||
if (!(archive instanceof ExplodedArchive)) {
|
||||
this.classPathArchives.add(archive);
|
||||
return;
|
||||
}
|
||||
this.classPathArchives.add(archive);
|
||||
this.classPathArchives.addAll(asList(archive.getNestedArchives(null, new ArchiveEntryFilter())));
|
||||
}
|
||||
|
||||
private List<Archive> getClassPathArchives(String path) throws Exception {
|
||||
String root = cleanupPath(handleUrl(path));
|
||||
List<Archive> lib = new ArrayList<>();
|
||||
File file = new File(root);
|
||||
if (!"/".equals(root)) {
|
||||
if (!isAbsolutePath(root)) {
|
||||
file = new File(PropertiesLauncher.this.home, root);
|
||||
}
|
||||
if (file.isDirectory()) {
|
||||
debug("Adding classpath entries from " + file);
|
||||
Archive archive = new ExplodedArchive(file, false);
|
||||
lib.add(archive);
|
||||
}
|
||||
}
|
||||
Archive archive = getArchive(file);
|
||||
if (archive != null) {
|
||||
debug("Adding classpath entries from archive " + archive.getUrl() + root);
|
||||
lib.add(archive);
|
||||
}
|
||||
List<Archive> nestedArchives = getNestedArchives(root);
|
||||
if (nestedArchives != null) {
|
||||
debug("Adding classpath entries from nested " + root);
|
||||
lib.addAll(nestedArchives);
|
||||
}
|
||||
return lib;
|
||||
}
|
||||
|
||||
private boolean isAbsolutePath(String root) {
|
||||
// Windows contains ":" others start with "/"
|
||||
return root.contains(":") || root.startsWith("/");
|
||||
}
|
||||
|
||||
private Archive getArchive(File file) throws IOException {
|
||||
if (isNestedArchivePath(file)) {
|
||||
return null;
|
||||
}
|
||||
String name = file.getName().toLowerCase(Locale.ENGLISH);
|
||||
if (name.endsWith(".jar") || name.endsWith(".zip")) {
|
||||
return getJarFileArchive(file);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isNestedArchivePath(File file) {
|
||||
return file.getPath().contains(NESTED_ARCHIVE_SEPARATOR);
|
||||
}
|
||||
|
||||
private List<Archive> getNestedArchives(String path) throws Exception {
|
||||
Archive parent = PropertiesLauncher.this.parent;
|
||||
String root = path;
|
||||
if (!root.equals("/") && root.startsWith("/")
|
||||
|| parent.getUrl().toURI().equals(PropertiesLauncher.this.home.toURI())) {
|
||||
// If home dir is same as parent archive, no need to add it twice.
|
||||
return null;
|
||||
}
|
||||
int index = root.indexOf('!');
|
||||
if (index != -1) {
|
||||
File file = new File(PropertiesLauncher.this.home, root.substring(0, index));
|
||||
if (root.startsWith("jar:file:")) {
|
||||
file = new File(root.substring("jar:file:".length(), index));
|
||||
}
|
||||
parent = getJarFileArchive(file);
|
||||
root = root.substring(index + 1);
|
||||
while (root.startsWith("/")) {
|
||||
root = root.substring(1);
|
||||
}
|
||||
}
|
||||
if (root.endsWith(".jar")) {
|
||||
File file = new File(PropertiesLauncher.this.home, root);
|
||||
if (file.exists()) {
|
||||
parent = getJarFileArchive(file);
|
||||
root = "";
|
||||
}
|
||||
}
|
||||
if (root.equals("/") || root.equals("./") || root.equals(".")) {
|
||||
// The prefix for nested jars is actually empty if it's at the root
|
||||
root = "";
|
||||
}
|
||||
EntryFilter filter = new PrefixMatchingArchiveFilter(root);
|
||||
List<Archive> archives = asList(parent.getNestedArchives(null, filter));
|
||||
if ((root == null || root.isEmpty() || ".".equals(root)) && !path.endsWith(".jar")
|
||||
&& parent != PropertiesLauncher.this.parent) {
|
||||
// You can't find the root with an entry filter so it has to be added
|
||||
// explicitly. But don't add the root of the parent archive.
|
||||
archives.add(parent);
|
||||
}
|
||||
return archives;
|
||||
}
|
||||
|
||||
private void addNestedEntries() {
|
||||
// The parent archive might have "BOOT-INF/lib/" and "BOOT-INF/classes/"
|
||||
// directories, meaning we are running from an executable JAR. We add nested
|
||||
// entries from there with low priority (i.e. at end).
|
||||
try {
|
||||
Iterator<Archive> archives = PropertiesLauncher.this.parent.getNestedArchives(null,
|
||||
JarLauncher.NESTED_ARCHIVE_ENTRY_FILTER);
|
||||
while (archives.hasNext()) {
|
||||
this.classPathArchives.add(archives.next());
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
private List<Archive> asList(Iterator<Archive> iterator) {
|
||||
List<Archive> list = new ArrayList<>();
|
||||
while (iterator.hasNext()) {
|
||||
list.add(iterator.next());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private JarFileArchive getJarFileArchive(File file) throws IOException {
|
||||
JarFileArchive archive = new JarFileArchive(file);
|
||||
this.jarFileArchives.add(archive);
|
||||
return archive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Archive> iterator() {
|
||||
return this.classPathArchives.iterator();
|
||||
}
|
||||
|
||||
void close() throws IOException {
|
||||
for (JarFileArchive archive : this.jarFileArchives) {
|
||||
archive.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience class for finding nested archives that have a prefix in their file path
|
||||
* (e.g. "lib/").
|
||||
*/
|
||||
private static final class PrefixMatchingArchiveFilter implements EntryFilter {
|
||||
|
||||
private final String prefix;
|
||||
|
||||
private final ArchiveEntryFilter filter = new ArchiveEntryFilter();
|
||||
|
||||
private PrefixMatchingArchiveFilter(String prefix) {
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Entry entry) {
|
||||
if (entry.isDirectory()) {
|
||||
return entry.getName().equals(this.prefix);
|
||||
}
|
||||
return entry.getName().startsWith(this.prefix) && this.filter.matches(entry);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience class for finding nested archives (archive entries that can be
|
||||
* classpath entries).
|
||||
*/
|
||||
private static final class ArchiveEntryFilter implements EntryFilter {
|
||||
|
||||
private static final String DOT_JAR = ".jar";
|
||||
|
||||
private static final String DOT_ZIP = ".zip";
|
||||
|
||||
@Override
|
||||
public boolean matches(Entry entry) {
|
||||
return entry.getName().endsWith(DOT_JAR) || entry.getName().endsWith(DOT_ZIP);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader;
|
||||
|
||||
import org.springframework.boot.loader.archive.Archive;
|
||||
|
||||
/**
|
||||
* {@link Launcher} for WAR based archives. This launcher for standard WAR archives.
|
||||
* Supports dependencies in {@code WEB-INF/lib} as well as {@code WEB-INF/lib-provided},
|
||||
* classes are loaded from {@code WEB-INF/classes}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class WarLauncher extends ExecutableArchiveLauncher {
|
||||
|
||||
public WarLauncher() {
|
||||
}
|
||||
|
||||
protected WarLauncher(Archive archive) {
|
||||
super(archive);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isPostProcessingClassPathArchives() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNestedArchive(Archive.Entry entry) {
|
||||
if (entry.isDirectory()) {
|
||||
return entry.getName().equals("WEB-INF/classes/");
|
||||
}
|
||||
return entry.getName().startsWith("WEB-INF/lib/") || entry.getName().startsWith("WEB-INF/lib-provided/");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getArchiveEntryPathPrefix() {
|
||||
return "WEB-INF/";
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
new WarLauncher().launch(args);
|
||||
}
|
||||
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.archive;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Iterator;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.springframework.boot.loader.Launcher;
|
||||
|
||||
/**
|
||||
* An archive that can be launched by the {@link Launcher}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.0.0
|
||||
* @see JarFileArchive
|
||||
*/
|
||||
public interface Archive extends Iterable<Archive.Entry>, AutoCloseable {
|
||||
|
||||
/**
|
||||
* Returns a URL that can be used to load the archive.
|
||||
* @return the archive URL
|
||||
* @throws MalformedURLException if the URL is malformed
|
||||
*/
|
||||
URL getUrl() throws MalformedURLException;
|
||||
|
||||
/**
|
||||
* Returns the manifest of the archive.
|
||||
* @return the manifest
|
||||
* @throws IOException if the manifest cannot be read
|
||||
*/
|
||||
Manifest getManifest() throws IOException;
|
||||
|
||||
/**
|
||||
* Returns nested {@link Archive}s for entries that match the specified filters.
|
||||
* @param searchFilter filter used to limit when additional sub-entry searching is
|
||||
* required or {@code null} if all entries should be considered.
|
||||
* @param includeFilter filter used to determine which entries should be included in
|
||||
* the result or {@code null} if all entries should be included
|
||||
* @return the nested archives
|
||||
* @throws IOException on IO error
|
||||
* @since 2.3.0
|
||||
*/
|
||||
Iterator<Archive> getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException;
|
||||
|
||||
/**
|
||||
* Return if the archive is exploded (already unpacked).
|
||||
* @return if the archive is exploded
|
||||
* @since 2.3.0
|
||||
*/
|
||||
default boolean isExploded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the {@code Archive}, releasing any open resources.
|
||||
* @throws Exception if an error occurs during close processing
|
||||
* @since 2.2.0
|
||||
*/
|
||||
@Override
|
||||
default void close() throws Exception {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single entry in the archive.
|
||||
*/
|
||||
interface Entry {
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the entry represents a directory.
|
||||
* @return if the entry is a directory
|
||||
*/
|
||||
boolean isDirectory();
|
||||
|
||||
/**
|
||||
* Returns the name of the entry.
|
||||
* @return the name of the entry
|
||||
*/
|
||||
String getName();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Strategy interface to filter {@link Entry Entries}.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface EntryFilter {
|
||||
|
||||
/**
|
||||
* Apply the jar entry filter.
|
||||
* @param entry the entry to filter
|
||||
* @return {@code true} if the filter matches
|
||||
*/
|
||||
boolean matches(Entry entry);
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,342 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.archive;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Deque;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Set;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
/**
|
||||
* {@link Archive} implementation backed by an exploded archive directory.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Madhura Bhave
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class ExplodedArchive implements Archive {
|
||||
|
||||
private static final Set<String> SKIPPED_NAMES = new HashSet<>(Arrays.asList(".", ".."));
|
||||
|
||||
private final File root;
|
||||
|
||||
private final boolean recursive;
|
||||
|
||||
private final File manifestFile;
|
||||
|
||||
private Manifest manifest;
|
||||
|
||||
/**
|
||||
* Create a new {@link ExplodedArchive} instance.
|
||||
* @param root the root directory
|
||||
*/
|
||||
public ExplodedArchive(File root) {
|
||||
this(root, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ExplodedArchive} instance.
|
||||
* @param root the root directory
|
||||
* @param recursive if recursive searching should be used to locate the manifest.
|
||||
* Defaults to {@code true}, directories with a large tree might want to set this to
|
||||
* {@code false}.
|
||||
*/
|
||||
public ExplodedArchive(File root, boolean recursive) {
|
||||
if (!root.exists() || !root.isDirectory()) {
|
||||
throw new IllegalArgumentException("Invalid source directory " + root);
|
||||
}
|
||||
this.root = root;
|
||||
this.recursive = recursive;
|
||||
this.manifestFile = getManifestFile(root);
|
||||
}
|
||||
|
||||
private File getManifestFile(File root) {
|
||||
File metaInf = new File(root, "META-INF");
|
||||
return new File(metaInf, "MANIFEST.MF");
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getUrl() throws MalformedURLException {
|
||||
return this.root.toURI().toURL();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Manifest getManifest() throws IOException {
|
||||
if (this.manifest == null && this.manifestFile.exists()) {
|
||||
try (FileInputStream inputStream = new FileInputStream(this.manifestFile)) {
|
||||
this.manifest = new Manifest(inputStream);
|
||||
}
|
||||
}
|
||||
return this.manifest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Archive> getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException {
|
||||
return new ArchiveIterator(this.root, this.recursive, searchFilter, includeFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated(since = "2.3.10", forRemoval = false)
|
||||
public Iterator<Entry> iterator() {
|
||||
return new EntryIterator(this.root, this.recursive, null, null);
|
||||
}
|
||||
|
||||
protected Archive getNestedArchive(Entry entry) {
|
||||
File file = ((FileEntry) entry).getFile();
|
||||
return (file.isDirectory() ? new ExplodedArchive(file) : new SimpleJarFileArchive((FileEntry) entry));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExploded() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
try {
|
||||
return getUrl().toString();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return "exploded archive";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* File based {@link Entry} {@link Iterator}.
|
||||
*/
|
||||
private abstract static class AbstractIterator<T> implements Iterator<T> {
|
||||
|
||||
private static final Comparator<File> entryComparator = Comparator.comparing(File::getAbsolutePath);
|
||||
|
||||
private final File root;
|
||||
|
||||
private final boolean recursive;
|
||||
|
||||
private final EntryFilter searchFilter;
|
||||
|
||||
private final EntryFilter includeFilter;
|
||||
|
||||
private final Deque<Iterator<File>> stack = new LinkedList<>();
|
||||
|
||||
private FileEntry current;
|
||||
|
||||
private final String rootUrl;
|
||||
|
||||
AbstractIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) {
|
||||
this.root = root;
|
||||
this.rootUrl = this.root.toURI().getPath();
|
||||
this.recursive = recursive;
|
||||
this.searchFilter = searchFilter;
|
||||
this.includeFilter = includeFilter;
|
||||
this.stack.add(listFiles(root));
|
||||
this.current = poll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return this.current != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T next() {
|
||||
FileEntry entry = this.current;
|
||||
if (entry == null) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
this.current = poll();
|
||||
return adapt(entry);
|
||||
}
|
||||
|
||||
private FileEntry poll() {
|
||||
while (!this.stack.isEmpty()) {
|
||||
while (this.stack.peek().hasNext()) {
|
||||
File file = this.stack.peek().next();
|
||||
if (SKIPPED_NAMES.contains(file.getName())) {
|
||||
continue;
|
||||
}
|
||||
FileEntry entry = getFileEntry(file);
|
||||
if (isListable(entry)) {
|
||||
this.stack.addFirst(listFiles(file));
|
||||
}
|
||||
if (this.includeFilter == null || this.includeFilter.matches(entry)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
this.stack.poll();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private FileEntry getFileEntry(File file) {
|
||||
URI uri = file.toURI();
|
||||
String name = uri.getPath().substring(this.rootUrl.length());
|
||||
try {
|
||||
return new FileEntry(name, file, uri.toURL());
|
||||
}
|
||||
catch (MalformedURLException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isListable(FileEntry entry) {
|
||||
return entry.isDirectory() && (this.recursive || entry.getFile().getParentFile().equals(this.root))
|
||||
&& (this.searchFilter == null || this.searchFilter.matches(entry))
|
||||
&& (this.includeFilter == null || !this.includeFilter.matches(entry));
|
||||
}
|
||||
|
||||
private Iterator<File> listFiles(File file) {
|
||||
File[] files = file.listFiles();
|
||||
if (files == null) {
|
||||
return Collections.emptyIterator();
|
||||
}
|
||||
Arrays.sort(files, entryComparator);
|
||||
return Arrays.asList(files).iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException("remove");
|
||||
}
|
||||
|
||||
protected abstract T adapt(FileEntry entry);
|
||||
|
||||
}
|
||||
|
||||
private static class EntryIterator extends AbstractIterator<Entry> {
|
||||
|
||||
EntryIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) {
|
||||
super(root, recursive, searchFilter, includeFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Entry adapt(FileEntry entry) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class ArchiveIterator extends AbstractIterator<Archive> {
|
||||
|
||||
ArchiveIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) {
|
||||
super(root, recursive, searchFilter, includeFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Archive adapt(FileEntry entry) {
|
||||
File file = entry.getFile();
|
||||
return (file.isDirectory() ? new ExplodedArchive(file) : new SimpleJarFileArchive(entry));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Entry} backed by a File.
|
||||
*/
|
||||
private static class FileEntry implements Entry {
|
||||
|
||||
private final String name;
|
||||
|
||||
private final File file;
|
||||
|
||||
private final URL url;
|
||||
|
||||
FileEntry(String name, File file, URL url) {
|
||||
this.name = name;
|
||||
this.file = file;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
File getFile() {
|
||||
return this.file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory() {
|
||||
return this.file.isDirectory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
URL getUrl() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Archive} implementation backed by a simple JAR file that doesn't itself
|
||||
* contain nested archives.
|
||||
*/
|
||||
private static class SimpleJarFileArchive implements Archive {
|
||||
|
||||
private final URL url;
|
||||
|
||||
SimpleJarFileArchive(FileEntry file) {
|
||||
this.url = file.getUrl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getUrl() throws MalformedURLException {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Manifest getManifest() throws IOException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Archive> getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter)
|
||||
throws IOException {
|
||||
return Collections.emptyIterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated(since = "2.3.10", forRemoval = false)
|
||||
public Iterator<Entry> iterator() {
|
||||
return Collections.emptyIterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
try {
|
||||
return getUrl().toString();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return "jar archive";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,310 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.archive;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.FileAttribute;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.UUID;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.springframework.boot.loader.jar.JarFile;
|
||||
|
||||
/**
|
||||
* {@link Archive} implementation backed by a {@link JarFile}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class JarFileArchive implements Archive {
|
||||
|
||||
private static final String UNPACK_MARKER = "UNPACK:";
|
||||
|
||||
private static final int BUFFER_SIZE = 32 * 1024;
|
||||
|
||||
private static final FileAttribute<?>[] NO_FILE_ATTRIBUTES = {};
|
||||
|
||||
private static final EnumSet<PosixFilePermission> DIRECTORY_PERMISSIONS = EnumSet.of(PosixFilePermission.OWNER_READ,
|
||||
PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE);
|
||||
|
||||
private static final EnumSet<PosixFilePermission> FILE_PERMISSIONS = EnumSet.of(PosixFilePermission.OWNER_READ,
|
||||
PosixFilePermission.OWNER_WRITE);
|
||||
|
||||
private final JarFile jarFile;
|
||||
|
||||
private URL url;
|
||||
|
||||
private Path tempUnpackDirectory;
|
||||
|
||||
public JarFileArchive(File file) throws IOException {
|
||||
this(file, file.toURI().toURL());
|
||||
}
|
||||
|
||||
public JarFileArchive(File file, URL url) throws IOException {
|
||||
this(new JarFile(file));
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public JarFileArchive(JarFile jarFile) {
|
||||
this.jarFile = jarFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getUrl() throws MalformedURLException {
|
||||
if (this.url != null) {
|
||||
return this.url;
|
||||
}
|
||||
return this.jarFile.getUrl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Manifest getManifest() throws IOException {
|
||||
return this.jarFile.getManifest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Archive> getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException {
|
||||
return new NestedArchiveIterator(this.jarFile.iterator(), searchFilter, includeFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated(since = "2.3.10", forRemoval = false)
|
||||
public Iterator<Entry> iterator() {
|
||||
return new EntryIterator(this.jarFile.iterator(), null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
this.jarFile.close();
|
||||
}
|
||||
|
||||
protected Archive getNestedArchive(Entry entry) throws IOException {
|
||||
JarEntry jarEntry = ((JarFileEntry) entry).getJarEntry();
|
||||
if (jarEntry.getComment().startsWith(UNPACK_MARKER)) {
|
||||
return getUnpackedNestedArchive(jarEntry);
|
||||
}
|
||||
try {
|
||||
JarFile jarFile = this.jarFile.getNestedJarFile(jarEntry);
|
||||
return new JarFileArchive(jarFile);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Failed to get nested archive for entry " + entry.getName(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Archive getUnpackedNestedArchive(JarEntry jarEntry) throws IOException {
|
||||
String name = jarEntry.getName();
|
||||
if (name.lastIndexOf('/') != -1) {
|
||||
name = name.substring(name.lastIndexOf('/') + 1);
|
||||
}
|
||||
Path path = getTempUnpackDirectory().resolve(name);
|
||||
if (!Files.exists(path) || Files.size(path) != jarEntry.getSize()) {
|
||||
unpack(jarEntry, path);
|
||||
}
|
||||
return new JarFileArchive(path.toFile(), path.toUri().toURL());
|
||||
}
|
||||
|
||||
private Path getTempUnpackDirectory() {
|
||||
if (this.tempUnpackDirectory == null) {
|
||||
Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir"));
|
||||
this.tempUnpackDirectory = createUnpackDirectory(tempDirectory);
|
||||
}
|
||||
return this.tempUnpackDirectory;
|
||||
}
|
||||
|
||||
private Path createUnpackDirectory(Path parent) {
|
||||
int attempts = 0;
|
||||
while (attempts++ < 1000) {
|
||||
String fileName = Paths.get(this.jarFile.getName()).getFileName().toString();
|
||||
Path unpackDirectory = parent.resolve(fileName + "-spring-boot-libs-" + UUID.randomUUID());
|
||||
try {
|
||||
createDirectory(unpackDirectory);
|
||||
return unpackDirectory;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Failed to create unpack directory in directory '" + parent + "'");
|
||||
}
|
||||
|
||||
private void unpack(JarEntry entry, Path path) throws IOException {
|
||||
createFile(path);
|
||||
path.toFile().deleteOnExit();
|
||||
try (InputStream inputStream = this.jarFile.getInputStream(entry);
|
||||
OutputStream outputStream = Files.newOutputStream(path, StandardOpenOption.WRITE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
outputStream.flush();
|
||||
}
|
||||
}
|
||||
|
||||
private void createDirectory(Path path) throws IOException {
|
||||
Files.createDirectory(path, getFileAttributes(path.getFileSystem(), DIRECTORY_PERMISSIONS));
|
||||
}
|
||||
|
||||
private void createFile(Path path) throws IOException {
|
||||
Files.createFile(path, getFileAttributes(path.getFileSystem(), FILE_PERMISSIONS));
|
||||
}
|
||||
|
||||
private FileAttribute<?>[] getFileAttributes(FileSystem fileSystem, EnumSet<PosixFilePermission> ownerReadWrite) {
|
||||
if (!fileSystem.supportedFileAttributeViews().contains("posix")) {
|
||||
return NO_FILE_ATTRIBUTES;
|
||||
}
|
||||
return new FileAttribute<?>[] { PosixFilePermissions.asFileAttribute(ownerReadWrite) };
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
try {
|
||||
return getUrl().toString();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return "jar archive";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for iterator implementations.
|
||||
*/
|
||||
private abstract static class AbstractIterator<T> implements Iterator<T> {
|
||||
|
||||
private final Iterator<JarEntry> iterator;
|
||||
|
||||
private final EntryFilter searchFilter;
|
||||
|
||||
private final EntryFilter includeFilter;
|
||||
|
||||
private Entry current;
|
||||
|
||||
AbstractIterator(Iterator<JarEntry> iterator, EntryFilter searchFilter, EntryFilter includeFilter) {
|
||||
this.iterator = iterator;
|
||||
this.searchFilter = searchFilter;
|
||||
this.includeFilter = includeFilter;
|
||||
this.current = poll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return this.current != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T next() {
|
||||
T result = adapt(this.current);
|
||||
this.current = poll();
|
||||
return result;
|
||||
}
|
||||
|
||||
private Entry poll() {
|
||||
while (this.iterator.hasNext()) {
|
||||
JarFileEntry candidate = new JarFileEntry(this.iterator.next());
|
||||
if ((this.searchFilter == null || this.searchFilter.matches(candidate))
|
||||
&& (this.includeFilter == null || this.includeFilter.matches(candidate))) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract T adapt(Entry entry);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Archive.Entry} iterator implementation backed by {@link JarEntry}.
|
||||
*/
|
||||
private static class EntryIterator extends AbstractIterator<Entry> {
|
||||
|
||||
EntryIterator(Iterator<JarEntry> iterator, EntryFilter searchFilter, EntryFilter includeFilter) {
|
||||
super(iterator, searchFilter, includeFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Entry adapt(Entry entry) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Nested {@link Archive} iterator implementation backed by {@link JarEntry}.
|
||||
*/
|
||||
private class NestedArchiveIterator extends AbstractIterator<Archive> {
|
||||
|
||||
NestedArchiveIterator(Iterator<JarEntry> iterator, EntryFilter searchFilter, EntryFilter includeFilter) {
|
||||
super(iterator, searchFilter, includeFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Archive adapt(Entry entry) {
|
||||
try {
|
||||
return getNestedArchive(entry);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Archive.Entry} implementation backed by a {@link JarEntry}.
|
||||
*/
|
||||
private static class JarFileEntry implements Entry {
|
||||
|
||||
private final JarEntry jarEntry;
|
||||
|
||||
JarFileEntry(JarEntry jarEntry) {
|
||||
this.jarEntry = jarEntry;
|
||||
}
|
||||
|
||||
JarEntry getJarEntry() {
|
||||
return this.jarEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory() {
|
||||
return this.jarEntry.isDirectory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.jarEntry.getName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.data;
|
||||
|
||||
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();
|
||||
|
||||
}
|
@ -1,262 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.data;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,255 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -1,258 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.boot.loader.data.RandomAccessData;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
AsciiBytes comment = new 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,222 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
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;
|
||||
|
||||
import org.springframework.boot.loader.data.RandomAccessData;
|
||||
|
||||
/**
|
||||
* 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 AsciiBytes SLASH = new AsciiBytes("/");
|
||||
|
||||
private static final byte[] NO_EXTRA = {};
|
||||
|
||||
private static final AsciiBytes NO_COMMENT = new AsciiBytes("");
|
||||
|
||||
private byte[] header;
|
||||
|
||||
private int headerOffset;
|
||||
|
||||
private AsciiBytes name;
|
||||
|
||||
private byte[] extra;
|
||||
|
||||
private AsciiBytes comment;
|
||||
|
||||
private long localHeaderOffset;
|
||||
|
||||
CentralDirectoryFileHeader() {
|
||||
}
|
||||
|
||||
CentralDirectoryFileHeader(byte[] header, int headerOffset, AsciiBytes name, byte[] extra, 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 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 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");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.loader.data.RandomAccessData;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
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();
|
||||
|
||||
}
|
@ -1,466 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
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 ioException) {
|
||||
log(false, "Unable to open fallback handler", ex);
|
||||
throw ioException;
|
||||
}
|
||||
log(true, "Unable to open fallback handler", ex);
|
||||
if (reason instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
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 AsciiBytes name;
|
||||
|
||||
private final AsciiBytes headerName;
|
||||
|
||||
private final JarFile jarFile;
|
||||
|
||||
private final long localHeaderOffset;
|
||||
|
||||
private volatile JarEntryCertification certification;
|
||||
|
||||
JarEntry(JarFile jarFile, int index, CentralDirectoryFileHeader header, 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -1,475 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
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.Enumeration;
|
||||
import java.util.Iterator;
|
||||
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;
|
||||
|
||||
import org.springframework.boot.loader.data.RandomAccessData;
|
||||
import org.springframework.boot.loader.data.RandomAccessDataFile;
|
||||
|
||||
/**
|
||||
* 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 = "org.springframework.boot.loader";
|
||||
|
||||
private static final AsciiBytes META_INF = new AsciiBytes("META-INF/");
|
||||
|
||||
private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new 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 final JarFileEntries entries;
|
||||
|
||||
private final Supplier<Manifest> manifestSupplier;
|
||||
|
||||
private SoftReference<Manifest> manifest;
|
||||
|
||||
private boolean signed;
|
||||
|
||||
private String comment;
|
||||
|
||||
private volatile boolean closed;
|
||||
|
||||
private volatile JarFileWrapper wrapper;
|
||||
|
||||
/**
|
||||
* 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());
|
||||
super.close();
|
||||
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 {
|
||||
this.rootFile.close();
|
||||
super.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) {
|
||||
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) {
|
||||
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 java.lang.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 jarEntry) {
|
||||
return this.entries.getInputStream(jarEntry);
|
||||
}
|
||||
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 {
|
||||
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());
|
||||
return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName(), entryData,
|
||||
JarFileType.NESTED_JAR);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
super.close();
|
||||
if (this.type == JarFileType.DIRECT) {
|
||||
this.rootFile.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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,491 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
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;
|
||||
|
||||
import org.springframework.boot.loader.data.RandomAccessData;
|
||||
|
||||
/**
|
||||
* 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 = Runtime.version().feature();
|
||||
|
||||
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<>(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) {
|
||||
AsciiBytes name = applyFilter(fileHeader.getName());
|
||||
if (name != null) {
|
||||
add(name, dataOffset);
|
||||
}
|
||||
}
|
||||
|
||||
private void add(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;
|
||||
AsciiBytes nameAlias = (entry instanceof JarEntry jarEntry) ? jarEntry.getAsciiBytesName()
|
||||
: new AsciiBytes(name.toString());
|
||||
while (version > BASE_VERSION) {
|
||||
T versionedEntry = doGetEntry("META-INF/versions/" + version + "/" + name, type, cacheEntry, nameAlias);
|
||||
if (versionedEntry != null) {
|
||||
return versionedEntry;
|
||||
}
|
||||
version--;
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
private boolean isMetaInfEntry(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,
|
||||
AsciiBytes nameAlias) {
|
||||
int hashCode = AsciiBytes.hashCode(name);
|
||||
T entry = getEntry(hashCode, name, NO_SUFFIX, type, cacheEntry, nameAlias);
|
||||
if (entry == null) {
|
||||
hashCode = AsciiBytes.hashCode(hashCode, SLASH);
|
||||
entry = getEntry(hashCode, name, SLASH, type, cacheEntry, nameAlias);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
private <T extends FileHeader> T getEntry(int hashCode, CharSequence name, char suffix, Class<T> type,
|
||||
boolean cacheEntry, AsciiBytes nameAlias) {
|
||||
int index = getFirstIndex(hashCode);
|
||||
while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) {
|
||||
T entry = getEntry(index, type, cacheEntry, nameAlias);
|
||||
if (entry.hasName(name, suffix)) {
|
||||
return entry;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T extends FileHeader> T getEntry(int index, Class<T> type, boolean cacheEntry, AsciiBytes nameAlias) {
|
||||
try {
|
||||
long offset = this.centralDirectoryOffsets.get(index);
|
||||
FileHeader cached = this.entriesCache.get(index);
|
||||
FileHeader entry = (cached != null) ? cached
|
||||
: CentralDirectoryFileHeader.fromRandomAccessData(this.centralDirectoryData, offset, this.filter);
|
||||
if (CentralDirectoryFileHeader.class.equals(entry.getClass()) && type.equals(JarEntry.class)) {
|
||||
entry = new JarEntry(this.jarFile, index, (CentralDirectoryFileHeader) entry, nameAlias);
|
||||
}
|
||||
if (cacheEntry && cached != entry) {
|
||||
this.entriesCache.put(index, entry);
|
||||
}
|
||||
return (T) entry;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private int getFirstIndex(int hashCode) {
|
||||
int index = Arrays.binarySearch(this.hashCodes, 0, this.size, hashCode);
|
||||
if (index < 0) {
|
||||
return -1;
|
||||
}
|
||||
while (index > 0 && this.hashCodes[index - 1] == hashCode) {
|
||||
index--;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
void clearCache() {
|
||||
this.entriesCache.clear();
|
||||
}
|
||||
|
||||
private AsciiBytes applyFilter(AsciiBytes name) {
|
||||
return (this.filter != null) ? this.filter.apply(name) : name;
|
||||
}
|
||||
|
||||
JarEntryCertification getCertification(JarEntry entry) throws IOException {
|
||||
JarEntryCertification[] certifications = this.certifications;
|
||||
if (certifications == null) {
|
||||
certifications = new JarEntryCertification[this.size];
|
||||
// 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;
|
||||
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 = 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
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;
|
||||
super.close();
|
||||
}
|
||||
|
||||
@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 file) {
|
||||
return file;
|
||||
}
|
||||
if (jarFile instanceof JarFileWrapper wrapper) {
|
||||
return unwrap(wrapper.parent);
|
||||
}
|
||||
throw new IllegalStateException("Not a JarFile or Wrapper");
|
||||
}
|
||||
|
||||
}
|
@ -1,393 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
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 final 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 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 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 {
|
||||
StringSequence spec = new 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(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);
|
||||
}
|
||||
|
||||
/**
|
||||
* A JarEntryName parsed from a URL String.
|
||||
*/
|
||||
static class JarEntryName {
|
||||
|
||||
private final StringSequence name;
|
||||
|
||||
private String contentType;
|
||||
|
||||
JarEntryName(StringSequence spec) {
|
||||
this.name = decode(spec);
|
||||
}
|
||||
|
||||
private StringSequence decode(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 StringSequence(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(StringSequence spec) {
|
||||
return get(spec, 0);
|
||||
}
|
||||
|
||||
static JarEntryName get(StringSequence spec, int beginIndex) {
|
||||
if (spec.length() <= beginIndex) {
|
||||
return EMPTY_JAR_ENTRY_NAME;
|
||||
}
|
||||
return new JarEntryName(spec.subSequence(beginIndex));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.Attributes.Name;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.springframework.boot.loader.zip.ZipContent;
|
||||
|
||||
/**
|
||||
* Info obtained from a {@link ZipContent} instance relating to the {@link Manifest}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ManifestInfo {
|
||||
|
||||
private static final Name MULTI_RELEASE = new Name("Multi-Release");
|
||||
|
||||
static final ManifestInfo NONE = new ManifestInfo(null, false);
|
||||
|
||||
private final Manifest manifest;
|
||||
|
||||
private volatile Boolean multiRelease;
|
||||
|
||||
/**
|
||||
* Create a new {@link ManifestInfo} instance.
|
||||
* @param manifest the jar manifest
|
||||
*/
|
||||
ManifestInfo(Manifest manifest) {
|
||||
this(manifest, null);
|
||||
}
|
||||
|
||||
private ManifestInfo(Manifest manifest, Boolean multiRelease) {
|
||||
this.manifest = manifest;
|
||||
this.multiRelease = multiRelease;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the manifest, if any.
|
||||
* @return the manifest or {@code null}
|
||||
*/
|
||||
Manifest getManifest() {
|
||||
return this.manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if this is a multi-release jar.
|
||||
* @return if the jar is multi-release
|
||||
*/
|
||||
boolean isMultiRelease() {
|
||||
if (this.manifest == null) {
|
||||
return false;
|
||||
}
|
||||
Boolean multiRelease = this.multiRelease;
|
||||
if (multiRelease != null) {
|
||||
return multiRelease;
|
||||
}
|
||||
Attributes attributes = this.manifest.getMainAttributes();
|
||||
multiRelease = attributes.containsKey(MULTI_RELEASE);
|
||||
this.multiRelease = multiRelease;
|
||||
return multiRelease;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
import org.springframework.boot.loader.zip.ZipContent;
|
||||
|
||||
/**
|
||||
* Info obtained from a {@link ZipContent} instance relating to the directories listed
|
||||
* under {@code META-INF/versions/}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class MetaInfVersionsInfo {
|
||||
|
||||
static final MetaInfVersionsInfo NONE = new MetaInfVersionsInfo(Collections.emptySet());
|
||||
|
||||
private static final String META_INF_VERSIONS = NestedJarFile.META_INF_VERSIONS;
|
||||
|
||||
private final int[] versions;
|
||||
|
||||
private final String[] directories;
|
||||
|
||||
private MetaInfVersionsInfo(Set<Integer> versions) {
|
||||
this.versions = versions.stream().mapToInt(Integer::intValue).toArray();
|
||||
this.directories = versions.stream().map((version) -> META_INF_VERSIONS + version + "/").toArray(String[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the versions listed under {@code META-INF/versions/} in ascending order.
|
||||
* @return the versions
|
||||
*/
|
||||
int[] versions() {
|
||||
return this.versions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the version directories in the same order as {@link #versions()}.
|
||||
* @return the version directories
|
||||
*/
|
||||
String[] directories() {
|
||||
return this.directories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get {@link MetaInfVersionsInfo} for the given {@link ZipContent}.
|
||||
* @param zipContent the zip content
|
||||
* @return the {@link MetaInfVersionsInfo}.
|
||||
*/
|
||||
static MetaInfVersionsInfo get(ZipContent zipContent) {
|
||||
return get(zipContent.size(), zipContent::getEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get {@link MetaInfVersionsInfo} for the given details.
|
||||
* @param size the number of entries
|
||||
* @param entries a function to get an entry from an index
|
||||
* @return the {@link MetaInfVersionsInfo}.
|
||||
*/
|
||||
static MetaInfVersionsInfo get(int size, IntFunction<ZipContent.Entry> entries) {
|
||||
Set<Integer> versions = new TreeSet<>();
|
||||
for (int i = 0; i < size; i++) {
|
||||
ZipContent.Entry contentEntry = entries.apply(i);
|
||||
if (contentEntry.hasNameStartingWith(META_INF_VERSIONS) && !contentEntry.isDirectory()) {
|
||||
String name = contentEntry.getName();
|
||||
int slash = name.indexOf('/', META_INF_VERSIONS.length());
|
||||
String version = name.substring(META_INF_VERSIONS.length(), slash);
|
||||
try {
|
||||
int versionNumber = Integer.parseInt(version);
|
||||
if (versionNumber >= NestedJarFile.BASE_VERSION) {
|
||||
versions.add(versionNumber);
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
return (!versions.isEmpty()) ? new MetaInfVersionsInfo(versions) : NONE;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,801 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.ref.Cleaner.Cleanable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.security.CodeSigner;
|
||||
import java.security.cert.Certificate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Enumeration;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.Spliterator;
|
||||
import java.util.Spliterators.AbstractSpliterator;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipException;
|
||||
|
||||
import org.springframework.boot.loader.log.DebugLogger;
|
||||
import org.springframework.boot.loader.ref.Cleaner;
|
||||
import org.springframework.boot.loader.zip.CloseableDataBlock;
|
||||
import org.springframework.boot.loader.zip.ZipContent;
|
||||
import org.springframework.boot.loader.zip.ZipContent.Entry;
|
||||
|
||||
/**
|
||||
* Extended variant of {@link JarFile} that behaves in the same way but can open nested
|
||||
* jars.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public class NestedJarFile extends JarFile {
|
||||
|
||||
private static final int DECIMAL = 10;
|
||||
|
||||
private static final String META_INF = "META-INF/";
|
||||
|
||||
static final String META_INF_VERSIONS = META_INF + "versions/";
|
||||
|
||||
static final int BASE_VERSION = baseVersion().feature();
|
||||
|
||||
private static final DebugLogger debug = DebugLogger.get(NestedJarFile.class);
|
||||
|
||||
private final Cleaner cleaner;
|
||||
|
||||
private final NestedJarFileResources resources;
|
||||
|
||||
private final Cleanable cleanup;
|
||||
|
||||
private final String name;
|
||||
|
||||
private final int version;
|
||||
|
||||
private volatile NestedJarEntry lastEntry;
|
||||
|
||||
private volatile boolean closed;
|
||||
|
||||
private volatile ManifestInfo manifestInfo;
|
||||
|
||||
private volatile MetaInfVersionsInfo metaInfVersionsInfo;
|
||||
|
||||
/**
|
||||
* Creates a new {@link NestedJarFile} instance to read from the specific
|
||||
* {@code File}.
|
||||
* @param file the jar file to be opened for reading
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
NestedJarFile(File file) throws IOException {
|
||||
this(file, null, null, false, Cleaner.instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link NestedJarFile} instance to read from the specific
|
||||
* {@code File}.
|
||||
* @param file the jar file to be opened for reading
|
||||
* @param nestedEntryName the nested entry name to open or {@code null}
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
public NestedJarFile(File file, String nestedEntryName) throws IOException {
|
||||
this(file, nestedEntryName, null, true, Cleaner.instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link NestedJarFile} instance to read from the specific
|
||||
* {@code File}.
|
||||
* @param file the jar file to be opened for reading
|
||||
* @param nestedEntryName the nested entry name to open or {@code null}
|
||||
* @param version the release version to use when opening a multi-release jar
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
public NestedJarFile(File file, String nestedEntryName, Runtime.Version version) throws IOException {
|
||||
this(file, nestedEntryName, version, true, Cleaner.instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link NestedJarFile} instance to read from the specific
|
||||
* {@code File}.
|
||||
* @param file the jar file to be opened for reading
|
||||
* @param nestedEntryName the nested entry name to open or {@code null}
|
||||
* @param version the release version to use when opening a multi-release jar
|
||||
* @param onlyNestedJars if <em>only</em> nested jars should be opened
|
||||
* @param cleaner the cleaner used to release resources
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
NestedJarFile(File file, String nestedEntryName, Runtime.Version version, boolean onlyNestedJars, Cleaner cleaner)
|
||||
throws IOException {
|
||||
super(file);
|
||||
if (onlyNestedJars && (nestedEntryName == null || nestedEntryName.isEmpty())) {
|
||||
throw new IllegalArgumentException("nestedEntryName must not be empty");
|
||||
}
|
||||
debug.log("Created nested jar file (%s, %s, %s)", file, nestedEntryName, version);
|
||||
this.cleaner = cleaner;
|
||||
this.resources = new NestedJarFileResources(file, nestedEntryName);
|
||||
this.cleanup = cleaner.register(this, this.resources);
|
||||
this.name = file.getPath() + ((nestedEntryName != null) ? "!/" + nestedEntryName : "");
|
||||
this.version = (version != null) ? version.feature() : baseVersion().feature();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Manifest getManifest() throws IOException {
|
||||
try {
|
||||
return this.resources.zipContent().getInfo(ManifestInfo.class, this::getManifestInfo).getManifest();
|
||||
}
|
||||
catch (UncheckedIOException ex) {
|
||||
throw ex.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<JarEntry> entries() {
|
||||
synchronized (this) {
|
||||
ensureOpen();
|
||||
return new JarEntriesEnumeration(this.resources.zipContent());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<JarEntry> stream() {
|
||||
synchronized (this) {
|
||||
ensureOpen();
|
||||
return streamContentEntries().map(NestedJarEntry::new);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<JarEntry> versionedStream() {
|
||||
synchronized (this) {
|
||||
ensureOpen();
|
||||
return streamContentEntries().map(this::getBaseName)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.map(this::getJarEntry)
|
||||
.filter(Objects::nonNull);
|
||||
}
|
||||
}
|
||||
|
||||
private Stream<ZipContent.Entry> streamContentEntries() {
|
||||
ZipContentEntriesSpliterator spliterator = new ZipContentEntriesSpliterator(this.resources.zipContent());
|
||||
return StreamSupport.stream(spliterator, false);
|
||||
}
|
||||
|
||||
private String getBaseName(ZipContent.Entry contentEntry) {
|
||||
String name = contentEntry.getName();
|
||||
if (!name.startsWith(META_INF_VERSIONS)) {
|
||||
return name;
|
||||
}
|
||||
int versionNumberStartIndex = META_INF_VERSIONS.length();
|
||||
int versionNumberEndIndex = (versionNumberStartIndex != -1) ? name.indexOf('/', versionNumberStartIndex) : -1;
|
||||
if (versionNumberEndIndex == -1 || versionNumberEndIndex == (name.length() - 1)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
int versionNumber = Integer.parseInt(name, versionNumberStartIndex, versionNumberEndIndex, DECIMAL);
|
||||
if (versionNumber > this.version) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
return name.substring(versionNumberEndIndex + 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JarEntry getJarEntry(String name) {
|
||||
return getNestedJarEntry(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JarEntry getEntry(String name) {
|
||||
return getNestedJarEntry(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if an entry with the given name exists.
|
||||
* @param name the name to check
|
||||
* @return if the entry exists
|
||||
*/
|
||||
public boolean hasEntry(String name) {
|
||||
NestedJarEntry lastEntry = this.lastEntry;
|
||||
if (lastEntry != null && name.equals(lastEntry.getName())) {
|
||||
return true;
|
||||
}
|
||||
ZipContent.Entry entry = getVersionedContentEntry(name);
|
||||
if (entry != null) {
|
||||
return false;
|
||||
}
|
||||
synchronized (this) {
|
||||
ensureOpen();
|
||||
return this.resources.zipContent().hasEntry(null, name);
|
||||
}
|
||||
}
|
||||
|
||||
private NestedJarEntry getNestedJarEntry(String name) {
|
||||
Objects.requireNonNull(name, "name");
|
||||
NestedJarEntry lastEntry = this.lastEntry;
|
||||
if (lastEntry != null && name.equals(lastEntry.getName())) {
|
||||
return lastEntry;
|
||||
}
|
||||
ZipContent.Entry entry = getVersionedContentEntry(name);
|
||||
entry = (entry != null) ? entry : getContentEntry(null, name);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
NestedJarEntry nestedJarEntry = new NestedJarEntry(entry, name);
|
||||
this.lastEntry = nestedJarEntry;
|
||||
return nestedJarEntry;
|
||||
}
|
||||
|
||||
private ZipContent.Entry getVersionedContentEntry(String name) {
|
||||
// NOTE: we can't call isMultiRelease() directly because it's a final method and
|
||||
// it inspects the container jar. We use ManifestInfo instead.
|
||||
if (BASE_VERSION >= this.version || name.startsWith(META_INF) || !getManifestInfo().isMultiRelease()) {
|
||||
return null;
|
||||
}
|
||||
MetaInfVersionsInfo metaInfVersionsInfo = getMetaInfVersionsInfo();
|
||||
int[] versions = metaInfVersionsInfo.versions();
|
||||
String[] directories = metaInfVersionsInfo.directories();
|
||||
for (int i = versions.length - 1; i >= 0; i--) {
|
||||
if (versions[i] <= this.version) {
|
||||
ZipContent.Entry entry = getContentEntry(directories[i], name);
|
||||
if (entry != null) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ZipContent.Entry getContentEntry(String namePrefix, String name) {
|
||||
synchronized (this) {
|
||||
ensureOpen();
|
||||
return this.resources.zipContent().getEntry(namePrefix, name);
|
||||
}
|
||||
}
|
||||
|
||||
private ManifestInfo getManifestInfo() {
|
||||
ManifestInfo manifestInfo = this.manifestInfo;
|
||||
if (manifestInfo != null) {
|
||||
return manifestInfo;
|
||||
}
|
||||
synchronized (this) {
|
||||
ensureOpen();
|
||||
manifestInfo = this.resources.zipContent().getInfo(ManifestInfo.class, this::getManifestInfo);
|
||||
}
|
||||
this.manifestInfo = manifestInfo;
|
||||
return manifestInfo;
|
||||
}
|
||||
|
||||
private ManifestInfo getManifestInfo(ZipContent zipContent) {
|
||||
ZipContent.Entry contentEntry = zipContent.getEntry(MANIFEST_NAME);
|
||||
if (contentEntry == null) {
|
||||
return ManifestInfo.NONE;
|
||||
}
|
||||
try {
|
||||
try (InputStream inputStream = getInputStream(contentEntry)) {
|
||||
Manifest manifest = new Manifest(inputStream);
|
||||
return new ManifestInfo(manifest);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private MetaInfVersionsInfo getMetaInfVersionsInfo() {
|
||||
MetaInfVersionsInfo metaInfVersionsInfo = this.metaInfVersionsInfo;
|
||||
if (metaInfVersionsInfo != null) {
|
||||
return metaInfVersionsInfo;
|
||||
}
|
||||
synchronized (this) {
|
||||
ensureOpen();
|
||||
metaInfVersionsInfo = this.resources.zipContent()
|
||||
.getInfo(MetaInfVersionsInfo.class, MetaInfVersionsInfo::get);
|
||||
}
|
||||
this.metaInfVersionsInfo = metaInfVersionsInfo;
|
||||
return metaInfVersionsInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream(ZipEntry entry) throws IOException {
|
||||
Objects.requireNonNull(entry, "entry");
|
||||
if (entry instanceof NestedJarEntry nestedJarEntry && nestedJarEntry.isOwnedBy(this)) {
|
||||
return getInputStream(nestedJarEntry.contentEntry());
|
||||
}
|
||||
return getInputStream(getNestedJarEntry(entry.getName()).contentEntry());
|
||||
}
|
||||
|
||||
private InputStream getInputStream(ZipContent.Entry contentEntry) throws IOException {
|
||||
int compression = contentEntry.getCompressionMethod();
|
||||
if (compression != ZipEntry.STORED && compression != ZipEntry.DEFLATED) {
|
||||
throw new ZipException("invalid compression method");
|
||||
}
|
||||
synchronized (this) {
|
||||
ensureOpen();
|
||||
InputStream inputStream = new JarEntryInputStream(contentEntry);
|
||||
try {
|
||||
if (compression == ZipEntry.DEFLATED) {
|
||||
inputStream = new JarEntryInflaterInputStream((JarEntryInputStream) inputStream, this.resources);
|
||||
}
|
||||
this.resources.addInputStream(inputStream);
|
||||
return inputStream;
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
inputStream.close();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getComment() {
|
||||
synchronized (this) {
|
||||
ensureOpen();
|
||||
return this.resources.zipContent().getComment();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
synchronized (this) {
|
||||
ensureOpen();
|
||||
return this.resources.zipContent().size();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.closed = true;
|
||||
synchronized (this) {
|
||||
try {
|
||||
this.cleanup.clean();
|
||||
}
|
||||
catch (UncheckedIOException ex) {
|
||||
throw ex.getCause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
private void ensureOpen() {
|
||||
if (this.closed) {
|
||||
throw new IllegalStateException("Zip file closed");
|
||||
}
|
||||
if (this.resources.zipContent() == null) {
|
||||
throw new IllegalStateException("The object is not initialized.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any internal caches.
|
||||
*/
|
||||
public void clearCache() {
|
||||
synchronized (this) {
|
||||
this.lastEntry = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An individual entry from a {@link NestedJarFile}.
|
||||
*/
|
||||
private class NestedJarEntry extends java.util.jar.JarEntry {
|
||||
|
||||
private static final IllegalStateException CANNOT_BE_MODIFIED_EXCEPTION = new IllegalStateException(
|
||||
"Neste jar entries cannot be modified");
|
||||
|
||||
private final ZipContent.Entry contentEntry;
|
||||
|
||||
private final String name;
|
||||
|
||||
private volatile boolean populated;
|
||||
|
||||
NestedJarEntry(Entry contentEntry) {
|
||||
this(contentEntry, contentEntry.getName());
|
||||
}
|
||||
|
||||
NestedJarEntry(ZipContent.Entry contentEntry, String name) {
|
||||
super(contentEntry.getName());
|
||||
this.contentEntry = contentEntry;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTime() {
|
||||
populate();
|
||||
return super.getTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDateTime getTimeLocal() {
|
||||
populate();
|
||||
return super.getTimeLocal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTime(long time) {
|
||||
throw CANNOT_BE_MODIFIED_EXCEPTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTimeLocal(LocalDateTime time) {
|
||||
throw CANNOT_BE_MODIFIED_EXCEPTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileTime getLastModifiedTime() {
|
||||
populate();
|
||||
return super.getLastModifiedTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZipEntry setLastModifiedTime(FileTime time) {
|
||||
throw CANNOT_BE_MODIFIED_EXCEPTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileTime getLastAccessTime() {
|
||||
populate();
|
||||
return super.getLastAccessTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZipEntry setLastAccessTime(FileTime time) {
|
||||
throw CANNOT_BE_MODIFIED_EXCEPTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileTime getCreationTime() {
|
||||
populate();
|
||||
return super.getCreationTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZipEntry setCreationTime(FileTime time) {
|
||||
throw CANNOT_BE_MODIFIED_EXCEPTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize() {
|
||||
return this.contentEntry.getUncompressedSize() & 0xFFFFFFFFL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSize(long size) {
|
||||
throw CANNOT_BE_MODIFIED_EXCEPTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCompressedSize() {
|
||||
populate();
|
||||
return super.getCompressedSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCompressedSize(long csize) {
|
||||
throw CANNOT_BE_MODIFIED_EXCEPTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCrc() {
|
||||
populate();
|
||||
return super.getCrc();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCrc(long crc) {
|
||||
throw CANNOT_BE_MODIFIED_EXCEPTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMethod() {
|
||||
populate();
|
||||
return super.getMethod();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMethod(int method) {
|
||||
throw CANNOT_BE_MODIFIED_EXCEPTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getExtra() {
|
||||
populate();
|
||||
return super.getExtra();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExtra(byte[] extra) {
|
||||
throw CANNOT_BE_MODIFIED_EXCEPTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getComment() {
|
||||
populate();
|
||||
return super.getComment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setComment(String comment) {
|
||||
throw CANNOT_BE_MODIFIED_EXCEPTION;
|
||||
}
|
||||
|
||||
boolean isOwnedBy(NestedJarFile nestedJarFile) {
|
||||
return NestedJarFile.this == nestedJarFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRealName() {
|
||||
return super.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Attributes getAttributes() throws IOException {
|
||||
Manifest manifest = getManifest();
|
||||
return (manifest != null) ? manifest.getAttributes(getName()) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Certificate[] getCertificates() {
|
||||
return getSecurityInfo().getCertificates(contentEntry());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodeSigner[] getCodeSigners() {
|
||||
return getSecurityInfo().getCodeSigners(contentEntry());
|
||||
}
|
||||
|
||||
private SecurityInfo getSecurityInfo() {
|
||||
return NestedJarFile.this.resources.zipContent().getInfo(SecurityInfo.class, SecurityInfo::get);
|
||||
}
|
||||
|
||||
ZipContent.Entry contentEntry() {
|
||||
return this.contentEntry;
|
||||
}
|
||||
|
||||
private void populate() {
|
||||
boolean populated = this.populated;
|
||||
if (!populated) {
|
||||
ZipEntry entry = this.contentEntry.as(ZipEntry::new);
|
||||
super.setMethod(entry.getMethod());
|
||||
super.setTime(entry.getTime());
|
||||
super.setCrc(entry.getCrc());
|
||||
super.setCompressedSize(entry.getCompressedSize());
|
||||
super.setSize(entry.getSize());
|
||||
super.setExtra(entry.getExtra());
|
||||
super.setComment(entry.getComment());
|
||||
this.populated = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Enumeration} of {@link NestedJarEntry} instances.
|
||||
*/
|
||||
private class JarEntriesEnumeration implements Enumeration<JarEntry> {
|
||||
|
||||
private final ZipContent zipContent;
|
||||
|
||||
private int cursor;
|
||||
|
||||
JarEntriesEnumeration(ZipContent zipContent) {
|
||||
this.zipContent = zipContent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMoreElements() {
|
||||
return this.cursor < this.zipContent.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NestedJarEntry nextElement() {
|
||||
if (!hasMoreElements()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
synchronized (NestedJarFile.this) {
|
||||
ensureOpen();
|
||||
return new NestedJarEntry(this.zipContent.getEntry(this.cursor++));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Spliterator} for {@link ZipContent.Entry} instances.
|
||||
*/
|
||||
private class ZipContentEntriesSpliterator extends AbstractSpliterator<ZipContent.Entry> {
|
||||
|
||||
private static final int ADDITIONAL_CHARACTERISTICS = Spliterator.ORDERED | Spliterator.DISTINCT
|
||||
| Spliterator.IMMUTABLE | Spliterator.NONNULL;
|
||||
|
||||
private final ZipContent zipContent;
|
||||
|
||||
private int cursor;
|
||||
|
||||
ZipContentEntriesSpliterator(ZipContent zipContent) {
|
||||
super(zipContent.size(), ADDITIONAL_CHARACTERISTICS);
|
||||
this.zipContent = zipContent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryAdvance(Consumer<? super ZipContent.Entry> action) {
|
||||
if (this.cursor < this.zipContent.size()) {
|
||||
synchronized (NestedJarFile.this) {
|
||||
ensureOpen();
|
||||
action.accept(this.zipContent.getEntry(this.cursor++));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link InputStream} to read jar entry content.
|
||||
*/
|
||||
private class JarEntryInputStream extends InputStream {
|
||||
|
||||
private final int uncompressedSize;
|
||||
|
||||
private final CloseableDataBlock content;
|
||||
|
||||
private long pos;
|
||||
|
||||
private long remaining;
|
||||
|
||||
private volatile boolean closed;
|
||||
|
||||
JarEntryInputStream(ZipContent.Entry entry) throws IOException {
|
||||
this.uncompressedSize = entry.getUncompressedSize();
|
||||
this.content = entry.openContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
byte[] b = new byte[1];
|
||||
return (read(b, 0, 1) == 1) ? b[0] & 0xFF : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int result;
|
||||
synchronized (NestedJarFile.this) {
|
||||
ensureOpen();
|
||||
ByteBuffer dst = ByteBuffer.wrap(b, off, len);
|
||||
int count = this.content.read(dst, this.pos);
|
||||
if (count > 0) {
|
||||
this.pos += count;
|
||||
this.remaining -= count;
|
||||
}
|
||||
result = count;
|
||||
}
|
||||
if (this.remaining == 0) {
|
||||
close();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
long result;
|
||||
synchronized (NestedJarFile.this) {
|
||||
result = (n > 0) ? maxForwardSkip(n) : maxBackwardSkip(n);
|
||||
this.pos += result;
|
||||
this.remaining -= result;
|
||||
}
|
||||
if (this.remaining == 0) {
|
||||
close();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private long maxForwardSkip(long n) {
|
||||
boolean willCauseOverflow = (this.pos + n) < 0;
|
||||
return (willCauseOverflow || n > this.remaining) ? this.remaining : n;
|
||||
}
|
||||
|
||||
private long maxBackwardSkip(long n) {
|
||||
return Math.max(-this.pos, n);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
return (this.remaining < Integer.MAX_VALUE) ? (int) this.remaining : Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
private void ensureOpen() throws ZipException {
|
||||
if (NestedJarFile.this.closed || this.closed) {
|
||||
throw new ZipException("ZipFile closed");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.closed = true;
|
||||
this.content.close();
|
||||
NestedJarFile.this.resources.removeInputStream(this);
|
||||
}
|
||||
|
||||
int getUncompressedSize() {
|
||||
return this.uncompressedSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ZipInflaterInputStream} to read and inflate jar entry content.
|
||||
*/
|
||||
private class JarEntryInflaterInputStream extends ZipInflaterInputStream {
|
||||
|
||||
private final Cleanable cleanup;
|
||||
|
||||
private volatile boolean closed;
|
||||
|
||||
JarEntryInflaterInputStream(JarEntryInputStream inputStream, NestedJarFileResources resources) {
|
||||
this(inputStream, resources, resources.getOrCreateInflater());
|
||||
}
|
||||
|
||||
private JarEntryInflaterInputStream(JarEntryInputStream inputStream, NestedJarFileResources resources,
|
||||
Inflater inflater) {
|
||||
super(inputStream, inflater, inputStream.getUncompressedSize());
|
||||
this.cleanup = NestedJarFile.this.cleaner.register(this, resources.createInflatorCleanupAction(inflater));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.closed = true;
|
||||
super.close();
|
||||
NestedJarFile.this.resources.removeInputStream(this);
|
||||
this.cleanup.clean();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.zip.Inflater;
|
||||
|
||||
import org.springframework.boot.loader.ref.Cleaner;
|
||||
import org.springframework.boot.loader.zip.ZipContent;
|
||||
|
||||
/**
|
||||
* Resources created managed and cleaned by a {@link NestedJarFile} instance and suitable
|
||||
* for registration with a {@link Cleaner}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class NestedJarFileResources implements Runnable {
|
||||
|
||||
private static final int INFLATER_CACHE_LIMIT = 20;
|
||||
|
||||
private ZipContent zipContent;
|
||||
|
||||
private final Set<InputStream> inputStreams = Collections.newSetFromMap(new WeakHashMap<>());
|
||||
|
||||
private Deque<Inflater> inflaterCache = new ArrayDeque<>();
|
||||
|
||||
/**
|
||||
* Create a new {@link NestedJarFileResources} instance.
|
||||
* @param file the source zip file
|
||||
* @param nestedEntryName the nested entry or {@code null}
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
NestedJarFileResources(File file, String nestedEntryName) throws IOException {
|
||||
this.zipContent = ZipContent.open(file.toPath(), nestedEntryName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the underling {@link ZipContent}.
|
||||
* @return the zip content
|
||||
*/
|
||||
ZipContent zipContent() {
|
||||
return this.zipContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a managed input stream resource.
|
||||
* @param inputStream the input stream
|
||||
*/
|
||||
void addInputStream(InputStream inputStream) {
|
||||
synchronized (this.inputStreams) {
|
||||
this.inputStreams.add(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a managed input stream resource.
|
||||
* @param inputStream the input stream
|
||||
*/
|
||||
void removeInputStream(InputStream inputStream) {
|
||||
synchronized (this.inputStreams) {
|
||||
this.inputStreams.remove(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link Runnable} action to cleanup the given inflater.
|
||||
* @param inflater the inflater to cleanup
|
||||
* @return the cleanup action
|
||||
*/
|
||||
Runnable createInflatorCleanupAction(Inflater inflater) {
|
||||
return () -> endOrCacheInflater(inflater);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get previously used {@link Inflater} from the cache, or create a new one.
|
||||
* @return a usable {@link Inflater}
|
||||
*/
|
||||
Inflater getOrCreateInflater() {
|
||||
Deque<Inflater> inflaterCache = this.inflaterCache;
|
||||
if (inflaterCache != null) {
|
||||
synchronized (inflaterCache) {
|
||||
Inflater inflater = this.inflaterCache.poll();
|
||||
if (inflater != null) {
|
||||
return inflater;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Inflater(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Either release the given {@link Inflater} by calling {@link Inflater#end()} or add
|
||||
* it to the cache for later reuse.
|
||||
* @param inflater the inflater to end or cache
|
||||
*/
|
||||
private void endOrCacheInflater(Inflater inflater) {
|
||||
Deque<Inflater> inflaterCache = this.inflaterCache;
|
||||
if (inflaterCache != null) {
|
||||
synchronized (inflaterCache) {
|
||||
if (this.inflaterCache == inflaterCache && inflaterCache.size() < INFLATER_CACHE_LIMIT) {
|
||||
inflater.reset();
|
||||
this.inflaterCache.add(inflater);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
inflater.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the {@link Cleaner} to free resources.
|
||||
* @see java.lang.Runnable#run()
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
releaseAll();
|
||||
}
|
||||
|
||||
private void releaseAll() {
|
||||
IOException exceptionChain = null;
|
||||
exceptionChain = releaseInflators(exceptionChain);
|
||||
exceptionChain = releaseInputStreams(exceptionChain);
|
||||
exceptionChain = releaseZipContent(exceptionChain);
|
||||
if (exceptionChain != null) {
|
||||
throw new UncheckedIOException(exceptionChain);
|
||||
}
|
||||
}
|
||||
|
||||
private IOException releaseInflators(IOException exceptionChain) {
|
||||
Deque<Inflater> inflaterCache = this.inflaterCache;
|
||||
if (inflaterCache != null) {
|
||||
try {
|
||||
synchronized (inflaterCache) {
|
||||
inflaterCache.forEach(Inflater::end);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.inflaterCache = null;
|
||||
}
|
||||
}
|
||||
return exceptionChain;
|
||||
}
|
||||
|
||||
private IOException releaseInputStreams(IOException exceptionChain) {
|
||||
synchronized (this.inputStreams) {
|
||||
for (InputStream inputStream : List.copyOf(this.inputStreams)) {
|
||||
try {
|
||||
inputStream.close();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
exceptionChain = addToExceptionChain(exceptionChain, ex);
|
||||
}
|
||||
}
|
||||
this.inputStreams.clear();
|
||||
}
|
||||
return exceptionChain;
|
||||
}
|
||||
|
||||
private IOException releaseZipContent(IOException exceptionChain) {
|
||||
ZipContent zipContent = this.zipContent;
|
||||
if (zipContent != null) {
|
||||
try {
|
||||
zipContent.close();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
exceptionChain = addToExceptionChain(exceptionChain, ex);
|
||||
}
|
||||
finally {
|
||||
this.zipContent = null;
|
||||
}
|
||||
}
|
||||
return exceptionChain;
|
||||
}
|
||||
|
||||
private IOException addToExceptionChain(IOException exceptionChain, IOException ex) {
|
||||
if (exceptionChain != null) {
|
||||
exceptionChain.addSuppressed(ex);
|
||||
return exceptionChain;
|
||||
}
|
||||
return ex;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.CodeSigner;
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarInputStream;
|
||||
|
||||
import org.springframework.boot.loader.zip.ZipContent;
|
||||
|
||||
/**
|
||||
* Security information ({@link Certificate} and {@link CodeSigner} details) for entries
|
||||
* in the jar.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class SecurityInfo {
|
||||
|
||||
static final SecurityInfo NONE = new SecurityInfo(null, null);
|
||||
|
||||
private final Certificate[][] certificateLookups;
|
||||
|
||||
private final CodeSigner[][] codeSignerLookups;
|
||||
|
||||
private SecurityInfo(Certificate[][] entryCertificates, CodeSigner[][] entryCodeSigners) {
|
||||
this.certificateLookups = entryCertificates;
|
||||
this.codeSignerLookups = entryCodeSigners;
|
||||
}
|
||||
|
||||
Certificate[] getCertificates(ZipContent.Entry contentEntry) {
|
||||
return (this.certificateLookups != null) ? clone(this.certificateLookups[contentEntry.getLookupIndex()]) : null;
|
||||
}
|
||||
|
||||
CodeSigner[] getCodeSigners(ZipContent.Entry contentEntry) {
|
||||
return (this.codeSignerLookups != null) ? clone(this.codeSignerLookups[contentEntry.getLookupIndex()]) : null;
|
||||
}
|
||||
|
||||
private <T> T[] clone(T[] array) {
|
||||
return (array != null) ? array.clone() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link SecurityInfo} for the given {@link ZipContent}.
|
||||
* @param content the zip content
|
||||
* @return the security info
|
||||
*/
|
||||
static SecurityInfo get(ZipContent content) {
|
||||
if (!content.hasJarSignatureFile()) {
|
||||
return NONE;
|
||||
}
|
||||
try {
|
||||
return load(content);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load security info from the jar file. We need to use {@link JarInputStream} to
|
||||
* obtain the security info since we don't have an actual real file to read. This
|
||||
* isn't that fast, but hopefully doesn't happen too often and the result is cached.
|
||||
* @param content the zip content
|
||||
* @return the security info
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
private static SecurityInfo load(ZipContent content) throws IOException {
|
||||
int size = content.size();
|
||||
boolean hasSecurityInfo = false;
|
||||
Certificate[][] entryCertificates = new Certificate[size][];
|
||||
CodeSigner[][] entryCodeSigners = new CodeSigner[size][];
|
||||
try (JarInputStream in = new JarInputStream(content.openRawZipData().asInputStream())) {
|
||||
JarEntry jarEntry = in.getNextJarEntry();
|
||||
while (jarEntry != null) {
|
||||
in.closeEntry(); // Close to trigger a read and set certs/signers
|
||||
Certificate[] certificates = jarEntry.getCertificates();
|
||||
CodeSigner[] codeSigners = jarEntry.getCodeSigners();
|
||||
if (certificates != null || codeSigners != null) {
|
||||
ZipContent.Entry contentEntry = content.getEntry(jarEntry.getName());
|
||||
if (contentEntry != null) {
|
||||
hasSecurityInfo = true;
|
||||
entryCertificates[contentEntry.getLookupIndex()] = certificates;
|
||||
entryCodeSigners[contentEntry.getLookupIndex()] = codeSigners;
|
||||
}
|
||||
}
|
||||
jarEntry = in.getNextJarEntry();
|
||||
}
|
||||
return (!hasSecurityInfo) ? NONE : new SecurityInfo(entryCertificates, entryCodeSigners);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.jar;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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 other)) {
|
||||
return false;
|
||||
}
|
||||
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,150 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.launch;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.security.CodeSource;
|
||||
import java.security.ProtectionDomain;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
/**
|
||||
* An archive that can be launched by the {@link Launcher}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public interface Archive extends AutoCloseable {
|
||||
|
||||
/**
|
||||
* Predicate that accepts all entries.
|
||||
*/
|
||||
Predicate<Entry> ALL_ENTRIES = (entry) -> true;
|
||||
|
||||
/**
|
||||
* Returns the manifest of the archive.
|
||||
* @return the manifest or {@code null}
|
||||
* @throws IOException if the manifest cannot be read
|
||||
*/
|
||||
Manifest getManifest() throws IOException;
|
||||
|
||||
/**
|
||||
* Returns classpath URLs for the archive that match the specified filter.
|
||||
* @param includeFilter filter used to determine which entries should be included.
|
||||
* @return the classpath URLs
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
default Set<URL> getClassPathUrls(Predicate<Entry> includeFilter) throws IOException {
|
||||
return getClassPathUrls(includeFilter, ALL_ENTRIES);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns classpath URLs for the archive that match the specified filters.
|
||||
* @param includeFilter filter used to determine which entries should be included
|
||||
* @param directorySearchFilter filter used to optimize tree walking for exploded
|
||||
* archives by determining if a directory needs to be searched or not
|
||||
* @return the classpath URLs
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
Set<URL> getClassPathUrls(Predicate<Entry> includeFilter, Predicate<Entry> directorySearchFilter)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* Returns if this archive is backed by an exploded archive directory.
|
||||
* @return if the archive is exploded
|
||||
*/
|
||||
default boolean isExploded() {
|
||||
return getRootDirectory() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root directory of this archive or {@code null} if the archive is not
|
||||
* backed by a directory.
|
||||
* @return the root directory
|
||||
*/
|
||||
default File getRootDirectory() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the {@code Archive}, releasing any open resources.
|
||||
* @throws Exception if an error occurs during close processing
|
||||
*/
|
||||
@Override
|
||||
default void close() throws Exception {
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create an appropriate {@link Archive} from the given
|
||||
* {@link Class} target.
|
||||
* @param target a target class that will be used to find the archive code source
|
||||
* @return an new {@link Archive} instance
|
||||
* @throws Exception if the archive cannot be created
|
||||
*/
|
||||
static Archive create(Class<?> target) throws Exception {
|
||||
return create(target.getProtectionDomain());
|
||||
}
|
||||
|
||||
static Archive create(ProtectionDomain protectionDomain) throws Exception {
|
||||
CodeSource codeSource = protectionDomain.getCodeSource();
|
||||
URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
|
||||
String path = (location != null) ? location.getSchemeSpecificPart() : null;
|
||||
if (path == null) {
|
||||
throw new IllegalStateException("Unable to determine code source archive");
|
||||
}
|
||||
return create(new File(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create an {@link Archive} from the given {@link File} target.
|
||||
* @param target a target {@link File} used to create the archive. May be a directory
|
||||
* or a jar file.
|
||||
* @return a new {@link Archive} instance.
|
||||
* @throws Exception if the archive cannot be created
|
||||
*/
|
||||
static Archive create(File target) throws Exception {
|
||||
if (!target.exists()) {
|
||||
throw new IllegalStateException("Unable to determine code source archive from " + target);
|
||||
}
|
||||
return (target.isDirectory() ? new ExplodedArchive(target) : new JarFileArchive(target));
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single entry in the archive.
|
||||
*/
|
||||
interface Entry {
|
||||
|
||||
/**
|
||||
* Returns the name of the entry.
|
||||
* @return the name of the entry
|
||||
*/
|
||||
String name();
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the entry represents a directory.
|
||||
* @return if the entry is a directory
|
||||
*/
|
||||
boolean isDirectory();
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.launch;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.springframework.boot.loader.launch.Archive.Entry;
|
||||
|
||||
/**
|
||||
* Base class for a {@link Launcher} backed by an executable archive.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Madhura Bhave
|
||||
* @author Scott Frederick
|
||||
* @since 3.2.0
|
||||
* @see JarLauncher
|
||||
* @see WarLauncher
|
||||
*/
|
||||
public abstract class ExecutableArchiveLauncher extends Launcher {
|
||||
|
||||
private static final String START_CLASS_ATTRIBUTE = "Start-Class";
|
||||
|
||||
protected static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index";
|
||||
|
||||
protected static final String DEFAULT_CLASSPATH_INDEX_FILE_NAME = "classpath.idx";
|
||||
|
||||
private final Archive archive;
|
||||
|
||||
private final ClassPathIndexFile classPathIndex;
|
||||
|
||||
public ExecutableArchiveLauncher() throws Exception {
|
||||
this(Archive.create(Launcher.class));
|
||||
}
|
||||
|
||||
protected ExecutableArchiveLauncher(Archive archive) throws Exception {
|
||||
this.archive = archive;
|
||||
this.classPathIndex = getClassPathIndex(this.archive);
|
||||
}
|
||||
|
||||
ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
|
||||
if (!archive.isExploded()) {
|
||||
return null; // Regular archives already have a defined order
|
||||
}
|
||||
String location = getClassPathIndexFileLocation(archive);
|
||||
return ClassPathIndexFile.loadIfPossible(archive.getRootDirectory(), location);
|
||||
}
|
||||
|
||||
private String getClassPathIndexFileLocation(Archive archive) throws IOException {
|
||||
Manifest manifest = archive.getManifest();
|
||||
Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null;
|
||||
String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null;
|
||||
return (location != null) ? location : getEntryPathPrefix() + DEFAULT_CLASSPATH_INDEX_FILE_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ClassLoader createClassLoader(Collection<URL> urls) throws Exception {
|
||||
if (this.classPathIndex != null) {
|
||||
urls = new ArrayList<>(urls);
|
||||
urls.addAll(this.classPathIndex.getUrls());
|
||||
}
|
||||
return super.createClassLoader(urls);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final Archive getArchive() {
|
||||
return this.archive;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getMainClass() throws Exception {
|
||||
Manifest manifest = this.archive.getManifest();
|
||||
String mainClass = (manifest != null) ? manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE) : null;
|
||||
if (mainClass == null) {
|
||||
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
|
||||
}
|
||||
return mainClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Set<URL> getClassPathUrls() throws Exception {
|
||||
return this.archive.getClassPathUrls(this::isIncludedOnClassPathAndNotIndexed, this::isSearchedDirectory);
|
||||
}
|
||||
|
||||
private boolean isIncludedOnClassPathAndNotIndexed(Entry entry) {
|
||||
if (!isIncludedOnClassPath(entry)) {
|
||||
return false;
|
||||
}
|
||||
return (this.classPathIndex == null) || !this.classPathIndex.containsEntry(entry.name());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the specified directory entry is a candidate for further searching.
|
||||
* @param entry the entry to check
|
||||
* @return {@code true} if the entry is a candidate for further searching
|
||||
*/
|
||||
protected boolean isSearchedDirectory(Archive.Entry entry) {
|
||||
return ((getEntryPathPrefix() == null) || entry.name().startsWith(getEntryPathPrefix()))
|
||||
&& !isIncludedOnClassPath(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the specified entry is a nested item that should be added to the
|
||||
* classpath.
|
||||
* @param entry the entry to check
|
||||
* @return {@code true} if the entry is a nested item (jar or directory)
|
||||
*/
|
||||
protected abstract boolean isIncludedOnClassPath(Archive.Entry entry);
|
||||
|
||||
/**
|
||||
* Return the path prefix for relevant entries in the archive.
|
||||
* @return the entry path prefix
|
||||
*/
|
||||
protected abstract String getEntryPathPrefix();
|
||||
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.launch;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
/**
|
||||
* {@link Archive} implementation backed by an exploded archive directory.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
class ExplodedArchive implements Archive {
|
||||
|
||||
private static final Object NO_MANIFEST = new Object();
|
||||
|
||||
private static final Set<String> SKIPPED_NAMES = Set.of(".", "..");
|
||||
|
||||
private static final Comparator<File> entryComparator = Comparator.comparing(File::getAbsolutePath);
|
||||
|
||||
private final File rootDirectory;
|
||||
|
||||
private final String rootUriPath;
|
||||
|
||||
private volatile Object manifest;
|
||||
|
||||
/**
|
||||
* Create a new {@link ExplodedArchive} instance.
|
||||
* @param rootDirectory the root directory
|
||||
*/
|
||||
ExplodedArchive(File rootDirectory) {
|
||||
if (!rootDirectory.exists() || !rootDirectory.isDirectory()) {
|
||||
throw new IllegalArgumentException("Invalid source directory " + rootDirectory);
|
||||
}
|
||||
this.rootDirectory = rootDirectory;
|
||||
this.rootUriPath = ExplodedArchive.this.rootDirectory.toURI().getPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Manifest getManifest() throws IOException {
|
||||
Object manifest = this.manifest;
|
||||
if (manifest == null) {
|
||||
manifest = loadManifest();
|
||||
this.manifest = manifest;
|
||||
}
|
||||
return (manifest != NO_MANIFEST) ? (Manifest) manifest : null;
|
||||
}
|
||||
|
||||
private Object loadManifest() throws IOException {
|
||||
File file = new File(this.rootDirectory, "META-INF/MANIFEST.MF");
|
||||
if (!file.exists()) {
|
||||
return NO_MANIFEST;
|
||||
}
|
||||
try (FileInputStream inputStream = new FileInputStream(file)) {
|
||||
return new Manifest(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<URL> getClassPathUrls(Predicate<Entry> includeFilter, Predicate<Entry> directorySearchFilter)
|
||||
throws IOException {
|
||||
Set<URL> urls = new LinkedHashSet<>();
|
||||
LinkedList<File> files = new LinkedList<>(listFiles(this.rootDirectory));
|
||||
while (!files.isEmpty()) {
|
||||
File file = files.poll();
|
||||
if (SKIPPED_NAMES.contains(file.getName())) {
|
||||
continue;
|
||||
}
|
||||
String entryName = file.toURI().getPath().substring(this.rootUriPath.length());
|
||||
Entry entry = new FileArchiveEntry(entryName, file);
|
||||
if (entry.isDirectory() && directorySearchFilter.test(entry)) {
|
||||
files.addAll(0, listFiles(file));
|
||||
}
|
||||
if (includeFilter.test(entry)) {
|
||||
urls.add(file.toURI().toURL());
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
private List<File> listFiles(File file) {
|
||||
File[] files = file.listFiles();
|
||||
if (files == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Arrays.sort(files, entryComparator);
|
||||
return Arrays.asList(files);
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getRootDirectory() {
|
||||
return this.rootDirectory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.rootDirectory.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Entry} backed by a File.
|
||||
*/
|
||||
private record FileArchiveEntry(String name, File file) implements Entry {
|
||||
|
||||
@Override
|
||||
public boolean isDirectory() {
|
||||
return this.file.isDirectory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,205 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.launch;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.FileAttribute;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.boot.loader.net.protocol.jar.JarUrl;
|
||||
|
||||
/**
|
||||
* {@link Archive} implementation backed by a {@link JarFile}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class JarFileArchive implements Archive {
|
||||
|
||||
private static final String UNPACK_MARKER = "UNPACK:";
|
||||
|
||||
private static final FileAttribute<?>[] NO_FILE_ATTRIBUTES = {};
|
||||
|
||||
private static final FileAttribute<?>[] DIRECTORY_PERMISSION_ATTRIBUTES = asFileAttributes(
|
||||
PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE);
|
||||
|
||||
private static final FileAttribute<?>[] FILE_PERMISSION_ATTRIBUTES = asFileAttributes(
|
||||
PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE);
|
||||
|
||||
private static final Path TEMP = Paths.get(System.getProperty("java.io.tmpdir"));
|
||||
|
||||
private final File file;
|
||||
|
||||
private final JarFile jarFile;
|
||||
|
||||
private volatile Path tempUnpackDirectory;
|
||||
|
||||
JarFileArchive(File file) throws IOException {
|
||||
this(file, new JarFile(file));
|
||||
}
|
||||
|
||||
private JarFileArchive(File file, JarFile jarFile) {
|
||||
this.file = file;
|
||||
this.jarFile = jarFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Manifest getManifest() throws IOException {
|
||||
return this.jarFile.getManifest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<URL> getClassPathUrls(Predicate<Entry> includeFilter, Predicate<Entry> directorySearchFilter)
|
||||
throws IOException {
|
||||
return this.jarFile.stream()
|
||||
.map(JarArchiveEntry::new)
|
||||
.filter(includeFilter)
|
||||
.map(this::getNestedJarUrl)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
private URL getNestedJarUrl(JarArchiveEntry archiveEntry) {
|
||||
try {
|
||||
JarEntry jarEntry = archiveEntry.jarEntry();
|
||||
String comment = jarEntry.getComment();
|
||||
if (comment != null && comment.startsWith(UNPACK_MARKER)) {
|
||||
return getUnpackedNestedJarUrl(jarEntry);
|
||||
}
|
||||
return JarUrl.create(this.file, jarEntry);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private URL getUnpackedNestedJarUrl(JarEntry jarEntry) throws IOException {
|
||||
String name = jarEntry.getName();
|
||||
if (name.lastIndexOf('/') != -1) {
|
||||
name = name.substring(name.lastIndexOf('/') + 1);
|
||||
}
|
||||
Path path = getTempUnpackDirectory().resolve(name);
|
||||
if (!Files.exists(path) || Files.size(path) != jarEntry.getSize()) {
|
||||
unpack(jarEntry, path);
|
||||
}
|
||||
return JarUrl.create(path.toFile());
|
||||
}
|
||||
|
||||
private Path getTempUnpackDirectory() {
|
||||
Path tempUnpackDirectory = this.tempUnpackDirectory;
|
||||
if (tempUnpackDirectory != null) {
|
||||
return tempUnpackDirectory;
|
||||
}
|
||||
synchronized (TEMP) {
|
||||
tempUnpackDirectory = this.tempUnpackDirectory;
|
||||
if (tempUnpackDirectory == null) {
|
||||
tempUnpackDirectory = createUnpackDirectory(TEMP);
|
||||
this.tempUnpackDirectory = tempUnpackDirectory;
|
||||
}
|
||||
}
|
||||
return tempUnpackDirectory;
|
||||
}
|
||||
|
||||
private Path createUnpackDirectory(Path parent) {
|
||||
int attempts = 0;
|
||||
String fileName = Paths.get(this.jarFile.getName()).getFileName().toString();
|
||||
while (attempts++ < 100) {
|
||||
Path unpackDirectory = parent.resolve(fileName + "-spring-boot-libs-" + UUID.randomUUID());
|
||||
try {
|
||||
createDirectory(unpackDirectory);
|
||||
return unpackDirectory;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Failed to create unpack directory in directory '" + parent + "'");
|
||||
}
|
||||
|
||||
private void createDirectory(Path path) throws IOException {
|
||||
Files.createDirectory(path, getFileAttributes(path, DIRECTORY_PERMISSION_ATTRIBUTES));
|
||||
}
|
||||
|
||||
private void unpack(JarEntry entry, Path path) throws IOException {
|
||||
createFile(path);
|
||||
path.toFile().deleteOnExit();
|
||||
try (InputStream in = this.jarFile.getInputStream(entry)) {
|
||||
Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
private void createFile(Path path) throws IOException {
|
||||
Files.createFile(path, getFileAttributes(path, FILE_PERMISSION_ATTRIBUTES));
|
||||
}
|
||||
|
||||
private FileAttribute<?>[] getFileAttributes(Path path, FileAttribute<?>[] permissionAttributes) {
|
||||
return (!supportsPosix(path.getFileSystem())) ? NO_FILE_ATTRIBUTES : permissionAttributes;
|
||||
}
|
||||
|
||||
private boolean supportsPosix(FileSystem fileSystem) {
|
||||
return fileSystem.supportedFileAttributeViews().contains("posix");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
this.jarFile.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.file.toString();
|
||||
}
|
||||
|
||||
private static FileAttribute<?>[] asFileAttributes(PosixFilePermission... permissions) {
|
||||
return new FileAttribute<?>[] { PosixFilePermissions.asFileAttribute(Set.of(permissions)) };
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Entry} implementation backed by a {@link JarEntry}.
|
||||
*/
|
||||
private record JarArchiveEntry(JarEntry jarEntry) implements Entry {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return this.jarEntry.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory() {
|
||||
return this.jarEntry.isDirectory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.launch;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.springframework.boot.loader.net.protocol.jar.JarUrlClassLoader;
|
||||
|
||||
/**
|
||||
* {@link ClassLoader} used by the {@link Launcher}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @author Andy Wilkinson
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public class LaunchedClassLoader extends JarUrlClassLoader {
|
||||
|
||||
private static final String JAR_MODE_PACKAGE_PREFIX = "org.springframework.boot.loader.jarmode.";
|
||||
|
||||
private static final String JAR_MODE_RUNNER_CLASS_NAME = JarModeRunner.class.getName();
|
||||
|
||||
static {
|
||||
ClassLoader.registerAsParallelCapable();
|
||||
}
|
||||
|
||||
private final boolean exploded;
|
||||
|
||||
private final Archive rootArchive;
|
||||
|
||||
private final Object definePackageLock = new Object();
|
||||
|
||||
private volatile DefinePackageCallType definePackageCallType;
|
||||
|
||||
/**
|
||||
* Create a new {@link LaunchedClassLoader} instance.
|
||||
* @param exploded if the underlying archive is exploded
|
||||
* @param urls the URLs from which to load classes and resources
|
||||
* @param parent the parent class loader for delegation
|
||||
*/
|
||||
public LaunchedClassLoader(boolean exploded, URL[] urls, ClassLoader parent) {
|
||||
this(exploded, null, urls, parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link LaunchedClassLoader} instance.
|
||||
* @param exploded if the underlying archive is exploded
|
||||
* @param rootArchive the root archive or {@code null}
|
||||
* @param urls the URLs from which to load classes and resources
|
||||
* @param parent the parent class loader for delegation
|
||||
*/
|
||||
public LaunchedClassLoader(boolean exploded, Archive rootArchive, URL[] urls, ClassLoader parent) {
|
||||
super(urls, parent);
|
||||
this.exploded = exploded;
|
||||
this.rootArchive = rootArchive;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
if (name.startsWith(JAR_MODE_PACKAGE_PREFIX) || name.equals(JAR_MODE_RUNNER_CLASS_NAME)) {
|
||||
try {
|
||||
Class<?> result = loadClassInLaunchedClassLoader(name);
|
||||
if (resolve) {
|
||||
resolveClass(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
return super.loadClass(name, resolve);
|
||||
}
|
||||
|
||||
private Class<?> loadClassInLaunchedClassLoader(String name) throws ClassNotFoundException {
|
||||
try {
|
||||
String internalName = name.replace('.', '/') + ".class";
|
||||
try (InputStream inputStream = getParent().getResourceAsStream(internalName);
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
if (inputStream == null) {
|
||||
throw new ClassNotFoundException(name);
|
||||
}
|
||||
inputStream.transferTo(outputStream);
|
||||
byte[] bytes = outputStream.toByteArray();
|
||||
Class<?> definedClass = defineClass(name, bytes, 0, bytes.length);
|
||||
definePackageIfNecessary(name);
|
||||
return definedClass;
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Package definePackage(String name, Manifest man, URL url) throws IllegalArgumentException {
|
||||
return (!this.exploded) ? super.definePackage(name, man, url) : definePackageForExploded(name, man, url);
|
||||
}
|
||||
|
||||
private Package definePackageForExploded(String name, Manifest man, URL url) {
|
||||
synchronized (this.definePackageLock) {
|
||||
return definePackage(DefinePackageCallType.MANIFEST, () -> super.definePackage(name, man, url));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Package definePackage(String name, String specTitle, String specVersion, String specVendor,
|
||||
String implTitle, String implVersion, String implVendor, URL sealBase) throws IllegalArgumentException {
|
||||
if (!this.exploded) {
|
||||
return super.definePackage(name, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor,
|
||||
sealBase);
|
||||
}
|
||||
return definePackageForExploded(name, sealBase, () -> super.definePackage(name, specTitle, specVersion,
|
||||
specVendor, implTitle, implVersion, implVendor, sealBase));
|
||||
}
|
||||
|
||||
private Package definePackageForExploded(String name, URL sealBase, Supplier<Package> call) {
|
||||
synchronized (this.definePackageLock) {
|
||||
if (this.definePackageCallType == null) {
|
||||
// We're not part of a call chain which means that the URLClassLoader
|
||||
// is trying to define a package for our exploded JAR. We use the
|
||||
// manifest version to ensure package attributes are set
|
||||
Manifest manifest = getManifest(this.rootArchive);
|
||||
if (manifest != null) {
|
||||
return definePackage(name, manifest, sealBase);
|
||||
}
|
||||
}
|
||||
return definePackage(DefinePackageCallType.ATTRIBUTES, call);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T definePackage(DefinePackageCallType type, Supplier<T> call) {
|
||||
DefinePackageCallType existingType = this.definePackageCallType;
|
||||
try {
|
||||
this.definePackageCallType = type;
|
||||
return call.get();
|
||||
}
|
||||
finally {
|
||||
this.definePackageCallType = existingType;
|
||||
}
|
||||
}
|
||||
|
||||
private Manifest getManifest(Archive archive) {
|
||||
try {
|
||||
return (archive != null) ? archive.getManifest() : null;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The different types of call made to define a package. We track these for exploded
|
||||
* jars so that we can detect packages that should have manifest attributes applied.
|
||||
*/
|
||||
private enum DefinePackageCallType {
|
||||
|
||||
/**
|
||||
* A define package call from a resource that has a manifest.
|
||||
*/
|
||||
MANIFEST,
|
||||
|
||||
/**
|
||||
* A define package call with a direct set of attributes.
|
||||
*/
|
||||
ATTRIBUTES
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.launch;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.loader.net.protocol.Handlers;
|
||||
|
||||
/**
|
||||
* Base class for launchers that can start an application with a fully configured
|
||||
* classpath.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public abstract class Launcher {
|
||||
|
||||
private static final String JAR_MODE_RUNNER_CLASS_NAME = JarModeRunner.class.getName();
|
||||
|
||||
/**
|
||||
* Launch the application. This method is the initial entry point that should be
|
||||
* called by a subclass {@code public static void main(String[] args)} method.
|
||||
* @param args the incoming arguments
|
||||
* @throws Exception if the application fails to launch
|
||||
*/
|
||||
protected void launch(String[] args) throws Exception {
|
||||
if (!isExploded()) {
|
||||
Handlers.register();
|
||||
}
|
||||
try {
|
||||
ClassLoader classLoader = createClassLoader(getClassPathUrls());
|
||||
String jarMode = System.getProperty("jarmode");
|
||||
String mainClassName = hasLength(jarMode) ? JAR_MODE_RUNNER_CLASS_NAME : getMainClass();
|
||||
launch(classLoader, mainClassName, args);
|
||||
}
|
||||
catch (UncheckedIOException ex) {
|
||||
throw ex.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasLength(String jarMode) {
|
||||
return (jarMode != null) && !jarMode.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a classloader for the specified archives.
|
||||
* @param urls the classpath URLs
|
||||
* @return the classloader
|
||||
* @throws Exception if the classloader cannot be created
|
||||
*/
|
||||
protected ClassLoader createClassLoader(Collection<URL> urls) throws Exception {
|
||||
return createClassLoader(urls.toArray(new URL[0]));
|
||||
}
|
||||
|
||||
private ClassLoader createClassLoader(URL[] urls) {
|
||||
ClassLoader parent = getClass().getClassLoader();
|
||||
return new LaunchedClassLoader(isExploded(), getArchive(), urls, parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch the application given the archive file and a fully configured classloader.
|
||||
* @param classLoader the classloader
|
||||
* @param mainClassName the main class to run
|
||||
* @param args the incoming arguments
|
||||
* @throws Exception if the launch fails
|
||||
*/
|
||||
protected void launch(ClassLoader classLoader, String mainClassName, String[] args) throws Exception {
|
||||
Thread.currentThread().setContextClassLoader(classLoader);
|
||||
Class<?> mainClass = Class.forName(mainClassName, false, classLoader);
|
||||
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
|
||||
mainMethod.setAccessible(true);
|
||||
mainMethod.invoke(null, new Object[] { args });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the launcher is running in an exploded mode. If this method returns
|
||||
* {@code true} then only regular JARs are supported and the additional URL and
|
||||
* ClassLoader support infrastructure can be optimized.
|
||||
* @return if the jar is exploded.
|
||||
*/
|
||||
protected boolean isExploded() {
|
||||
Archive archive = getArchive();
|
||||
return (archive != null) && archive.isExploded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the archive being launched or {@code null} if there is no archive.
|
||||
* @return the launched archive
|
||||
*/
|
||||
protected abstract Archive getArchive();
|
||||
|
||||
/**
|
||||
* Returns the main class that should be launched.
|
||||
* @return the name of the main class
|
||||
* @throws Exception if the main class cannot be obtained
|
||||
*/
|
||||
protected abstract String getMainClass() throws Exception;
|
||||
|
||||
/**
|
||||
* Returns the archives that will be used to construct the class path.
|
||||
* @return the class path archives
|
||||
* @throws Exception if the class path archives cannot be obtained
|
||||
*/
|
||||
protected abstract Set<URL> getClassPathUrls() throws Exception;
|
||||
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.launch;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Internal helper class adapted from Spring Framework for resolving placeholders in
|
||||
* texts.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Rob Harrop
|
||||
* @author Dave Syer
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class SystemPropertyUtils {
|
||||
|
||||
private static final String PLACEHOLDER_PREFIX = "${";
|
||||
|
||||
private static final String PLACEHOLDER_SUFFIX = "}";
|
||||
|
||||
private static final String VALUE_SEPARATOR = ":";
|
||||
|
||||
private static final String SIMPLE_PREFIX = PLACEHOLDER_PREFIX.substring(1);
|
||||
|
||||
private SystemPropertyUtils() {
|
||||
}
|
||||
|
||||
static String resolvePlaceholders(Properties properties, String text) {
|
||||
return (text != null) ? parseStringValue(properties, text, text, new HashSet<>()) : null;
|
||||
}
|
||||
|
||||
private static String parseStringValue(Properties properties, String value, String current,
|
||||
Set<String> visitedPlaceholders) {
|
||||
StringBuilder result = new StringBuilder(current);
|
||||
int startIndex = current.indexOf(PLACEHOLDER_PREFIX);
|
||||
while (startIndex != -1) {
|
||||
int endIndex = findPlaceholderEndIndex(result, startIndex);
|
||||
if (endIndex == -1) {
|
||||
startIndex = -1;
|
||||
continue;
|
||||
}
|
||||
String placeholder = result.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex);
|
||||
String originalPlaceholder = placeholder;
|
||||
if (!visitedPlaceholders.add(originalPlaceholder)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
|
||||
}
|
||||
placeholder = parseStringValue(properties, value, placeholder, visitedPlaceholders);
|
||||
String propertyValue = resolvePlaceholder(properties, value, placeholder);
|
||||
if (propertyValue == null) {
|
||||
int separatorIndex = placeholder.indexOf(VALUE_SEPARATOR);
|
||||
if (separatorIndex != -1) {
|
||||
String actualPlaceholder = placeholder.substring(0, separatorIndex);
|
||||
String defaultValue = placeholder.substring(separatorIndex + VALUE_SEPARATOR.length());
|
||||
propertyValue = resolvePlaceholder(properties, value, actualPlaceholder);
|
||||
propertyValue = (propertyValue != null) ? propertyValue : defaultValue;
|
||||
}
|
||||
}
|
||||
if (propertyValue != null) {
|
||||
propertyValue = parseStringValue(properties, value, propertyValue, visitedPlaceholders);
|
||||
result.replace(startIndex, endIndex + PLACEHOLDER_SUFFIX.length(), propertyValue);
|
||||
startIndex = result.indexOf(PLACEHOLDER_PREFIX, startIndex + propertyValue.length());
|
||||
}
|
||||
else {
|
||||
startIndex = result.indexOf(PLACEHOLDER_PREFIX, endIndex + PLACEHOLDER_SUFFIX.length());
|
||||
}
|
||||
visitedPlaceholders.remove(originalPlaceholder);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private static String resolvePlaceholder(Properties properties, String text, String placeholderName) {
|
||||
String propertyValue = getProperty(placeholderName, null, text);
|
||||
if (propertyValue != null) {
|
||||
return propertyValue;
|
||||
}
|
||||
return (properties != null) ? properties.getProperty(placeholderName) : null;
|
||||
}
|
||||
|
||||
static String getProperty(String key) {
|
||||
return getProperty(key, null, "");
|
||||
}
|
||||
|
||||
private static String getProperty(String key, String defaultValue, String text) {
|
||||
try {
|
||||
String value = System.getProperty(key);
|
||||
value = (value != null) ? value : System.getenv(key);
|
||||
value = (value != null) ? value : System.getenv(key.replace('.', '_'));
|
||||
value = (value != null) ? value : System.getenv(key.toUpperCase(Locale.ENGLISH).replace('.', '_'));
|
||||
return (value != null) ? value : defaultValue;
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
System.err.println("Could not resolve key '" + key + "' in '" + text
|
||||
+ "' as system property or in environment: " + ex);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
|
||||
int index = startIndex + PLACEHOLDER_PREFIX.length();
|
||||
int withinNestedPlaceholder = 0;
|
||||
while (index < buf.length()) {
|
||||
if (substringMatch(buf, index, PLACEHOLDER_SUFFIX)) {
|
||||
if (withinNestedPlaceholder > 0) {
|
||||
withinNestedPlaceholder--;
|
||||
index = index + PLACEHOLDER_SUFFIX.length();
|
||||
}
|
||||
else {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
else if (substringMatch(buf, index, SIMPLE_PREFIX)) {
|
||||
withinNestedPlaceholder++;
|
||||
index = index + SIMPLE_PREFIX.length();
|
||||
}
|
||||
else {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static boolean substringMatch(CharSequence str, int index, CharSequence substring) {
|
||||
for (int j = 0; j < substring.length(); j++) {
|
||||
int i = index + j;
|
||||
if (i >= str.length() || str.charAt(i) != substring.charAt(j)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.log;
|
||||
|
||||
/**
|
||||
* Simple logger class used for {@link System#err} debugging.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public abstract sealed class DebugLogger {
|
||||
|
||||
private static final String ENABLED_PROPERTY = "loader.debug";
|
||||
|
||||
private static final DebugLogger disabled;
|
||||
static {
|
||||
disabled = Boolean.getBoolean(ENABLED_PROPERTY) ? null : new DisabledDebugLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message.
|
||||
* @param message the message to log
|
||||
*/
|
||||
public abstract void log(String message);
|
||||
|
||||
/**
|
||||
* Log a formatted message.
|
||||
* @param message the message to log
|
||||
* @param arg1 the first format argument
|
||||
*/
|
||||
public abstract void log(String message, Object arg1);
|
||||
|
||||
/**
|
||||
* Log a formatted message.
|
||||
* @param message the message to log
|
||||
* @param arg1 the first format argument
|
||||
* @param arg2 the second format argument
|
||||
*/
|
||||
public abstract void log(String message, Object arg1, Object arg2);
|
||||
|
||||
/**
|
||||
* Log a formatted message.
|
||||
* @param message the message to log
|
||||
* @param arg1 the first format argument
|
||||
* @param arg2 the second format argument
|
||||
* @param arg3 the third format argument
|
||||
*/
|
||||
public abstract void log(String message, Object arg1, Object arg2, Object arg3);
|
||||
|
||||
/**
|
||||
* Log a formatted message.
|
||||
* @param message the message to log
|
||||
* @param arg1 the first format argument
|
||||
* @param arg2 the second format argument
|
||||
* @param arg3 the third format argument
|
||||
* @param arg4 the fourth format argument
|
||||
*/
|
||||
public abstract void log(String message, Object arg1, Object arg2, Object arg3, Object arg4);
|
||||
|
||||
/**
|
||||
* Get a {@link DebugLogger} to log messages for the given source class.
|
||||
* @param sourceClass the source class
|
||||
* @return a {@link DebugLogger} instance
|
||||
*/
|
||||
public static DebugLogger get(Class<?> sourceClass) {
|
||||
return (disabled != null) ? disabled : new SystemErrDebugLogger(sourceClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link DebugLogger} used for disabled logging that does nothing.
|
||||
*/
|
||||
private static final class DisabledDebugLogger extends DebugLogger {
|
||||
|
||||
@Override
|
||||
public void log(String message) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String message, Object arg1) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String message, Object arg1, Object arg2) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String message, Object arg1, Object arg2, Object arg3) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String message, Object arg1, Object arg2, Object arg3, Object arg4) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link DebugLogger} that prints messages to {@link System#err}.
|
||||
*/
|
||||
private static final class SystemErrDebugLogger extends DebugLogger {
|
||||
|
||||
private final String prefix;
|
||||
|
||||
SystemErrDebugLogger(Class<?> sourceClass) {
|
||||
this.prefix = "LOADER: " + sourceClass + " : ";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String message) {
|
||||
print(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String message, Object arg1) {
|
||||
print(message.formatted(arg1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String message, Object arg1, Object arg2) {
|
||||
print(message.formatted(arg1, arg2));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String message, Object arg1, Object arg2, Object arg3) {
|
||||
print(message.formatted(arg1, arg2, arg3));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String message, Object arg1, Object arg2, Object arg3, Object arg4) {
|
||||
print(message.formatted(arg1, arg2, arg3, arg4));
|
||||
}
|
||||
|
||||
private void print(String message) {
|
||||
System.err.println(this.prefix + message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.protocol;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLStreamHandler;
|
||||
import java.net.URLStreamHandlerFactory;
|
||||
|
||||
/**
|
||||
* Utility used to register loader {@link URLStreamHandler URL handlers}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public final class Handlers {
|
||||
|
||||
private static final String PROTOCOL_HANDLER_PACKAGES = "java.protocol.handler.pkgs";
|
||||
|
||||
private static final String PACKAGE = Handlers.class.getPackageName();
|
||||
|
||||
private Handlers() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a {@literal 'java.protocol.handler.pkgs'} property so that a
|
||||
* {@link URLStreamHandler} will be located to deal with jar URLs.
|
||||
*/
|
||||
public static void register() {
|
||||
String packages = System.getProperty(PROTOCOL_HANDLER_PACKAGES, "");
|
||||
packages = (!packages.isEmpty() && !packages.contains(PACKAGE)) ? packages + "|" + PACKAGE : PACKAGE;
|
||||
System.setProperty(PROTOCOL_HANDLER_PACKAGES, packages);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.protocol.jar;
|
||||
|
||||
/**
|
||||
* Internal utility used by the {@link Handler} to canonicalize paths. This implementation
|
||||
* should behave the same as the canonicalization functions in
|
||||
* {@code sun.net.www.protocol.jar.Handler}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class Canonicalizer {
|
||||
|
||||
private Canonicalizer() {
|
||||
}
|
||||
|
||||
static String canonicalizeAfter(String path, int pos) {
|
||||
int pathLength = path.length();
|
||||
boolean noDotSlash = path.indexOf("./", pos) == -1;
|
||||
if (pos >= pathLength || (noDotSlash && path.charAt(pathLength - 1) != '.')) {
|
||||
return path;
|
||||
}
|
||||
String before = path.substring(0, pos);
|
||||
String after = path.substring(pos);
|
||||
return before + canonicalize(after);
|
||||
}
|
||||
|
||||
static String canonicalize(String path) {
|
||||
path = removeEmbeddedSlashDotDotSlash(path);
|
||||
path = removedEmbdeddedSlashDotSlash(path);
|
||||
path = removeTrailingSlashDotDot(path);
|
||||
path = removeTrailingSlashDot(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static String removeEmbeddedSlashDotDotSlash(String path) {
|
||||
int index;
|
||||
while ((index = path.indexOf("/../")) >= 0) {
|
||||
int priorSlash = path.lastIndexOf('/', index - 1);
|
||||
String after = path.substring(index + 3);
|
||||
path = (priorSlash >= 0) ? path.substring(0, priorSlash) + after : after;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private static String removedEmbdeddedSlashDotSlash(String path) {
|
||||
int index;
|
||||
while ((index = path.indexOf("/./")) >= 0) {
|
||||
String before = path.substring(0, index);
|
||||
String after = path.substring(index + 2);
|
||||
path = before + after;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private static String removeTrailingSlashDot(String path) {
|
||||
return (!path.endsWith("/.")) ? path : path.substring(0, path.length() - 1);
|
||||
}
|
||||
|
||||
private static String removeTrailingSlashDotDot(String path) {
|
||||
int index;
|
||||
while (path.endsWith("/..")) {
|
||||
index = path.indexOf("/..");
|
||||
int priorSlash = path.lastIndexOf('/', index - 1);
|
||||
path = (priorSlash >= 0) ? path.substring(0, priorSlash + 1) : path.substring(0, index);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.protocol.jar;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
|
||||
/**
|
||||
* {@link URLStreamHandler} alternative to {@code sun.net.www.protocol.jar.Handler} with
|
||||
* optimized support for nested jars.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.2.0
|
||||
* @see org.springframework.boot.loader.net.protocol.Handlers
|
||||
*/
|
||||
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 PROTOCOL = "jar";
|
||||
|
||||
private static final String SEPARATOR = "!/";
|
||||
|
||||
static final Handler INSTANCE = new Handler();
|
||||
|
||||
@Override
|
||||
protected URLConnection openConnection(URL url) throws IOException {
|
||||
return JarUrlConnection.open(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void parseURL(URL url, String spec, int start, int limit) {
|
||||
if (spec.regionMatches(true, start, "jar:", 0, 4)) {
|
||||
throw new IllegalStateException("Nested JAR URLs are not supported");
|
||||
}
|
||||
int anchorIndex = spec.indexOf('#', limit);
|
||||
String path = extractPath(url, spec, start, limit, anchorIndex);
|
||||
String ref = (anchorIndex != -1) ? spec.substring(anchorIndex + 1) : null;
|
||||
setURL(url, PROTOCOL, "", -1, null, null, path, null, ref);
|
||||
}
|
||||
|
||||
private String extractPath(URL url, String spec, int start, int limit, int anchorIndex) {
|
||||
if (anchorIndex == start) {
|
||||
return extractAnchorOnlyPath(url);
|
||||
}
|
||||
if (spec.length() >= 4 && spec.regionMatches(true, 0, "jar:", 0, 4)) {
|
||||
return extractAbsolutePath(spec, start, limit);
|
||||
}
|
||||
return extractRelativePath(url, spec, start, limit);
|
||||
}
|
||||
|
||||
private String extractAnchorOnlyPath(URL url) {
|
||||
return url.getPath();
|
||||
}
|
||||
|
||||
private String extractAbsolutePath(String spec, int start, int limit) {
|
||||
int indexOfSeparator = indexOfSeparator(spec, start, limit);
|
||||
if (indexOfSeparator == -1) {
|
||||
throw new IllegalStateException("no !/ in spec");
|
||||
}
|
||||
String innerUrl = spec.substring(start, indexOfSeparator);
|
||||
assertInnerUrlIsNotMalformed(spec, innerUrl);
|
||||
return spec.substring(start, limit);
|
||||
}
|
||||
|
||||
private String extractRelativePath(URL url, String spec, int start, int limit) {
|
||||
String contextPath = extractContextPath(url, spec, start);
|
||||
String path = contextPath + spec.substring(start, limit);
|
||||
return Canonicalizer.canonicalizeAfter(path, indexOfSeparator(path) + 1);
|
||||
}
|
||||
|
||||
private String extractContextPath(URL url, String spec, int start) {
|
||||
String contextPath = url.getPath();
|
||||
if (spec.charAt(start) == '/') {
|
||||
int indexOfContextPathSeparator = indexOfSeparator(contextPath);
|
||||
if (indexOfContextPathSeparator == -1) {
|
||||
throw new IllegalStateException("malformed context url:%s: no !/".formatted(url));
|
||||
}
|
||||
return contextPath.substring(0, indexOfContextPathSeparator + 1);
|
||||
}
|
||||
int lastSlash = contextPath.lastIndexOf('/');
|
||||
if (lastSlash == -1) {
|
||||
throw new IllegalStateException("malformed context url:%s".formatted(url));
|
||||
}
|
||||
return contextPath.substring(0, lastSlash + 1);
|
||||
}
|
||||
|
||||
private void assertInnerUrlIsNotMalformed(String spec, String innerUrl) {
|
||||
if (innerUrl.startsWith("nested:")) {
|
||||
org.springframework.boot.loader.net.protocol.nested.Handler.assertUrlIsNotMalformed(innerUrl);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
new URL(innerUrl);
|
||||
}
|
||||
catch (MalformedURLException ex) {
|
||||
throw new IllegalStateException("invalid url: %s (%s)".formatted(spec, ex));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int hashCode(URL url) {
|
||||
String protocol = url.getProtocol();
|
||||
int hash = (protocol != null) ? protocol.hashCode() : 0;
|
||||
String file = url.getFile();
|
||||
int indexOfSeparator = file.indexOf(SEPARATOR);
|
||||
if (indexOfSeparator == -1) {
|
||||
return hash + file.hashCode();
|
||||
}
|
||||
String fileWithoutEntry = file.substring(0, indexOfSeparator);
|
||||
try {
|
||||
hash += new URL(fileWithoutEntry).hashCode();
|
||||
}
|
||||
catch (MalformedURLException ex) {
|
||||
hash += fileWithoutEntry.hashCode();
|
||||
}
|
||||
String entry = file.substring(indexOfSeparator + 2);
|
||||
return hash + entry.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean sameFile(URL url1, URL url2) {
|
||||
if (!url1.getProtocol().equals(PROTOCOL) || !url2.getProtocol().equals(PROTOCOL)) {
|
||||
return false;
|
||||
}
|
||||
String file1 = url1.getFile();
|
||||
String file2 = url2.getFile();
|
||||
int indexOfSeparator1 = file1.indexOf(SEPARATOR);
|
||||
int indexOfSeparator2 = file2.indexOf(SEPARATOR);
|
||||
if (indexOfSeparator1 == -1 || indexOfSeparator2 == -1) {
|
||||
return super.sameFile(url1, url2);
|
||||
}
|
||||
String entry1 = file1.substring(indexOfSeparator1 + 2);
|
||||
String entry2 = file2.substring(indexOfSeparator2 + 2);
|
||||
if (!entry1.equals(entry2)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
URL innerUrl1 = new URL(file1.substring(0, indexOfSeparator1));
|
||||
URL innerUrl2 = new URL(file2.substring(0, indexOfSeparator2));
|
||||
if (!super.sameFile(innerUrl1, innerUrl2)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (MalformedURLException unused) {
|
||||
return super.sameFile(url1, url2);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int indexOfSeparator(String spec) {
|
||||
return indexOfSeparator(spec, 0, spec.length());
|
||||
}
|
||||
|
||||
static int indexOfSeparator(String spec, int start, int limit) {
|
||||
for (int i = limit - 1; i >= start; i--) {
|
||||
if (spec.charAt(i) == '!' && (i + 1) < limit && spec.charAt(i + 1) == '/') {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any internal caches.
|
||||
*/
|
||||
public static void clearCache() {
|
||||
JarFileUrlKey.clearCache();
|
||||
JarUrlConnection.clearCache();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.protocol.jar;
|
||||
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.net.URL;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Utility to generate a string key from a jar file {@link URL} that can be used as a
|
||||
* cache key.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class JarFileUrlKey {
|
||||
|
||||
private static volatile SoftReference<Map<URL, String>> cache;
|
||||
|
||||
private JarFileUrlKey() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link JarFileUrlKey} for the given URL.
|
||||
* @param url the source URL
|
||||
* @return a {@link JarFileUrlKey} instance
|
||||
*/
|
||||
static String get(URL url) {
|
||||
Map<URL, String> cache = (JarFileUrlKey.cache != null) ? JarFileUrlKey.cache.get() : null;
|
||||
if (cache == null) {
|
||||
cache = new ConcurrentHashMap<>();
|
||||
JarFileUrlKey.cache = new SoftReference<>(cache);
|
||||
}
|
||||
return cache.computeIfAbsent(url, JarFileUrlKey::create);
|
||||
}
|
||||
|
||||
private static String create(URL url) {
|
||||
StringBuilder value = new StringBuilder();
|
||||
String protocol = url.getProtocol();
|
||||
String host = url.getHost();
|
||||
int port = (url.getPort() != -1) ? url.getPort() : url.getDefaultPort();
|
||||
String file = url.getFile();
|
||||
value.append(protocol.toLowerCase());
|
||||
value.append(":");
|
||||
if (host != null && !host.isEmpty()) {
|
||||
value.append(host.toLowerCase());
|
||||
value.append((port != -1) ? ":" + port : "");
|
||||
}
|
||||
value.append((file != null) ? file : "");
|
||||
if ("runtime".equals(url.getRef())) {
|
||||
value.append("#runtime");
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
static void clearCache() {
|
||||
cache = null;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.protocol.jar;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.jar.JarEntry;
|
||||
|
||||
/**
|
||||
* Utility class with factory methods that can be used to create JAR URLs.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public final class JarUrl {
|
||||
|
||||
private JarUrl() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new jar URL.
|
||||
* @param file the jar file
|
||||
* @return a jar file URL
|
||||
*/
|
||||
public static URL create(File file) {
|
||||
return create(file, (String) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new jar URL.
|
||||
* @param file the jar file
|
||||
* @param nestedEntry the nested entry or {@code null}
|
||||
* @return a jar file URL
|
||||
*/
|
||||
public static URL create(File file, JarEntry nestedEntry) {
|
||||
return create(file, (nestedEntry != null) ? nestedEntry.getName() : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new jar URL.
|
||||
* @param file the jar file
|
||||
* @param nestedEntryName the nested entry name or {@code null}
|
||||
* @return a jar file URL
|
||||
*/
|
||||
public static URL create(File file, String nestedEntryName) {
|
||||
return create(file, nestedEntryName, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new jar URL.
|
||||
* @param file the jar file
|
||||
* @param nestedEntryName the nested entry name or {@code null}
|
||||
* @param path the path within the jar or nested jar
|
||||
* @return a jar file URL
|
||||
*/
|
||||
public static URL create(File file, String nestedEntryName, String path) {
|
||||
try {
|
||||
path = (path != null) ? path : "";
|
||||
return new URL(null, "jar:" + getJarReference(file, nestedEntryName) + "!/" + path, Handler.INSTANCE);
|
||||
}
|
||||
catch (MalformedURLException ex) {
|
||||
throw new IllegalStateException("Unable to create JarFileArchive URL", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getJarReference(File file, String nestedEntryName) {
|
||||
String jarFilePath = file.toURI().getPath();
|
||||
return (nestedEntryName != null) ? "nested:" + jarFilePath + "/!" + nestedEntryName : "file:" + jarFilePath;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,290 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.protocol.jar;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.JarURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.net.URLConnection;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
import org.springframework.boot.loader.jar.NestedJarFile;
|
||||
import org.springframework.boot.loader.launch.LaunchedClassLoader;
|
||||
|
||||
/**
|
||||
* {@link URLClassLoader} with optimized support for Jar URLs.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public abstract class JarUrlClassLoader extends URLClassLoader {
|
||||
|
||||
private final URL[] urls;
|
||||
|
||||
private final boolean hasJarUrls;
|
||||
|
||||
private final Map<URL, JarFile> jarFiles = new ConcurrentHashMap<>();
|
||||
|
||||
private final Set<String> undefinablePackages = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
/**
|
||||
* Create a new {@link LaunchedClassLoader} instance.
|
||||
* @param urls the URLs from which to load classes and resources
|
||||
* @param parent the parent class loader for delegation
|
||||
*/
|
||||
public JarUrlClassLoader(URL[] urls, ClassLoader parent) {
|
||||
super(urls, parent);
|
||||
this.urls = urls;
|
||||
this.hasJarUrls = Arrays.stream(urls).anyMatch(this::isJarUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL findResource(String name) {
|
||||
if (!this.hasJarUrls) {
|
||||
return super.findResource(name);
|
||||
}
|
||||
Optimizations.enable(false);
|
||||
try {
|
||||
return super.findResource(name);
|
||||
}
|
||||
finally {
|
||||
Optimizations.disable();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<URL> findResources(String name) throws IOException {
|
||||
if (!this.hasJarUrls) {
|
||||
return super.findResources(name);
|
||||
}
|
||||
Optimizations.enable(false);
|
||||
try {
|
||||
return new OptimizedEnumeration(super.findResources(name));
|
||||
}
|
||||
finally {
|
||||
Optimizations.disable();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
if (!this.hasJarUrls) {
|
||||
return super.loadClass(name, resolve);
|
||||
}
|
||||
Optimizations.enable(true);
|
||||
try {
|
||||
try {
|
||||
definePackageIfNecessary(name);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
tolerateRaceConditionDueToBeingParallelCapable(ex, name);
|
||||
}
|
||||
return super.loadClass(name, resolve);
|
||||
}
|
||||
finally {
|
||||
Optimizations.disable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a package before a {@code findClass} call is made. This is necessary to
|
||||
* ensure that the appropriate manifest for nested JARs is associated with the
|
||||
* package.
|
||||
* @param className the class name being found
|
||||
*/
|
||||
protected final void definePackageIfNecessary(String className) {
|
||||
if (className.startsWith("java.")) {
|
||||
return;
|
||||
}
|
||||
int lastDot = className.lastIndexOf('.');
|
||||
if (lastDot >= 0) {
|
||||
String packageName = className.substring(0, lastDot);
|
||||
if (getDefinedPackage(packageName) == null) {
|
||||
try {
|
||||
definePackage(className, packageName);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
tolerateRaceConditionDueToBeingParallelCapable(ex, packageName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void definePackage(String className, String packageName) {
|
||||
if (this.undefinablePackages.contains(packageName)) {
|
||||
return;
|
||||
}
|
||||
String packageEntryName = packageName.replace('.', '/') + "/";
|
||||
String classEntryName = className.replace('.', '/') + ".class";
|
||||
for (URL url : this.urls) {
|
||||
try {
|
||||
JarFile jarFile = getJarFile(url);
|
||||
if (jarFile != null) {
|
||||
if (hasEntry(jarFile, classEntryName) && hasEntry(jarFile, packageEntryName)
|
||||
&& jarFile.getManifest() != null) {
|
||||
definePackage(packageName, jarFile.getManifest(), url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
this.undefinablePackages.add(packageName);
|
||||
}
|
||||
|
||||
private void tolerateRaceConditionDueToBeingParallelCapable(IllegalArgumentException ex, String packageName)
|
||||
throws AssertionError {
|
||||
if (getDefinedPackage(packageName) == null) {
|
||||
// This should never happen as the IllegalArgumentException indicates that the
|
||||
// package has already been defined and, therefore, getDefinedPackage(name)
|
||||
// should not have returned null.
|
||||
throw new AssertionError(
|
||||
"Package %s has already been defined but it could not be found".formatted(packageName), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasEntry(JarFile jarFile, String name) {
|
||||
return (jarFile instanceof NestedJarFile nestedJarFile) ? nestedJarFile.hasEntry(name)
|
||||
: jarFile.getEntry(name) != null;
|
||||
}
|
||||
|
||||
private JarFile getJarFile(URL url) throws IOException {
|
||||
JarFile jarFile = this.jarFiles.get(url);
|
||||
if (jarFile != null) {
|
||||
return jarFile;
|
||||
}
|
||||
URLConnection connection = url.openConnection();
|
||||
if (!(connection instanceof JarURLConnection)) {
|
||||
return null;
|
||||
}
|
||||
connection.setUseCaches(false);
|
||||
jarFile = ((JarURLConnection) connection).getJarFile();
|
||||
synchronized (this.jarFiles) {
|
||||
JarFile previous = this.jarFiles.putIfAbsent(url, jarFile);
|
||||
if (previous != null) {
|
||||
jarFile.close();
|
||||
jarFile = previous;
|
||||
}
|
||||
}
|
||||
return jarFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any caches. This method is called reflectively by
|
||||
* {@code ClearCachesApplicationListener}.
|
||||
*/
|
||||
public void clearCache() {
|
||||
Handler.clearCache();
|
||||
org.springframework.boot.loader.net.protocol.nested.Handler.clearCache();
|
||||
try {
|
||||
clearJarFiles();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// Ignore
|
||||
}
|
||||
for (URL url : this.urls) {
|
||||
if (isJarUrl(url)) {
|
||||
clearCache(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearCache(URL url) {
|
||||
try {
|
||||
URLConnection connection = url.openConnection();
|
||||
if (connection instanceof JarURLConnection jarUrlConnection) {
|
||||
clearCache(jarUrlConnection);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
private void clearCache(JarURLConnection connection) throws IOException {
|
||||
JarFile jarFile = connection.getJarFile();
|
||||
if (jarFile instanceof NestedJarFile nestedJarFile) {
|
||||
nestedJarFile.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isJarUrl(URL url) {
|
||||
return "jar".equals(url.getProtocol());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
super.close();
|
||||
clearJarFiles();
|
||||
}
|
||||
|
||||
private void clearJarFiles() throws IOException {
|
||||
synchronized (this.jarFiles) {
|
||||
for (JarFile jarFile : this.jarFiles.values()) {
|
||||
jarFile.close();
|
||||
}
|
||||
this.jarFiles.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Enumeration} that uses fast connections.
|
||||
*/
|
||||
private static class OptimizedEnumeration implements Enumeration<URL> {
|
||||
|
||||
private final Enumeration<URL> delegate;
|
||||
|
||||
OptimizedEnumeration(Enumeration<URL> delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMoreElements() {
|
||||
Optimizations.enable(false);
|
||||
try {
|
||||
return this.delegate.hasMoreElements();
|
||||
}
|
||||
finally {
|
||||
Optimizations.disable();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL nextElement() {
|
||||
Optimizations.enable(false);
|
||||
try {
|
||||
return this.delegate.nextElement();
|
||||
}
|
||||
finally {
|
||||
Optimizations.disable();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,399 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.protocol.jar;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
import java.security.Permission;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
import org.springframework.boot.loader.jar.NestedJarFile;
|
||||
import org.springframework.boot.loader.net.util.UrlDecoder;
|
||||
|
||||
/**
|
||||
* {@link java.net.JarURLConnection} alternative to
|
||||
* {@code sun.net.www.protocol.jar.JarURLConnection} with optimized support for nested
|
||||
* jars.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Rostyslav Dudka
|
||||
*/
|
||||
final class JarUrlConnection extends java.net.JarURLConnection {
|
||||
|
||||
static final UrlJarFiles jarFiles = new UrlJarFiles();
|
||||
|
||||
static final InputStream emptyInputStream = new ByteArrayInputStream(new byte[0]);
|
||||
|
||||
static final FileNotFoundException FILE_NOT_FOUND_EXCEPTION = new FileNotFoundException(
|
||||
"Jar file or entry not found");
|
||||
|
||||
private static final URL NOT_FOUND_URL;
|
||||
|
||||
static final JarUrlConnection NOT_FOUND_CONNECTION;
|
||||
static {
|
||||
try {
|
||||
NOT_FOUND_URL = new URL("jar:", null, 0, "nested:!/", new EmptyUrlStreamHandler());
|
||||
NOT_FOUND_CONNECTION = new JarUrlConnection(() -> FILE_NOT_FOUND_EXCEPTION);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private final String entryName;
|
||||
|
||||
private final Supplier<FileNotFoundException> notFound;
|
||||
|
||||
private JarFile jarFile;
|
||||
|
||||
private URLConnection jarFileConnection;
|
||||
|
||||
private JarEntry jarEntry;
|
||||
|
||||
private String contentType;
|
||||
|
||||
private JarUrlConnection(URL url) throws IOException {
|
||||
super(url);
|
||||
this.entryName = getEntryName();
|
||||
this.notFound = null;
|
||||
this.jarFileConnection = getJarFileURL().openConnection();
|
||||
this.jarFileConnection.setUseCaches(this.useCaches);
|
||||
}
|
||||
|
||||
private JarUrlConnection(Supplier<FileNotFoundException> notFound) throws IOException {
|
||||
super(NOT_FOUND_URL);
|
||||
this.entryName = null;
|
||||
this.notFound = notFound;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JarFile getJarFile() throws IOException {
|
||||
connect();
|
||||
return this.jarFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JarEntry getJarEntry() throws IOException {
|
||||
connect();
|
||||
return this.jarEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getContentLength() {
|
||||
long contentLength = getContentLengthLong();
|
||||
return (contentLength <= Integer.MAX_VALUE) ? (int) contentLength : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentLengthLong() {
|
||||
try {
|
||||
connect();
|
||||
return (this.jarEntry != null) ? this.jarEntry.getSize() : this.jarFileConnection.getContentLengthLong();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
if (this.contentType == null) {
|
||||
this.contentType = deduceContentType();
|
||||
}
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
private String deduceContentType() {
|
||||
String type = (this.entryName != null) ? null : "x-java/jar";
|
||||
type = (type != null) ? type : deduceContentTypeFromStream();
|
||||
type = (type != null) ? type : deduceContentTypeFromEntryName();
|
||||
return (type != null) ? type : "content/unknown";
|
||||
}
|
||||
|
||||
private String deduceContentTypeFromStream() {
|
||||
try {
|
||||
connect();
|
||||
try (InputStream in = this.jarFile.getInputStream(this.jarEntry)) {
|
||||
return guessContentTypeFromStream(new BufferedInputStream(in));
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String deduceContentTypeFromEntryName() {
|
||||
return guessContentTypeFromName(this.entryName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeaderField(String name) {
|
||||
return (this.jarFileConnection != null) ? this.jarFileConnection.getHeaderField(name) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getContent() throws IOException {
|
||||
connect();
|
||||
return (this.entryName != null) ? super.getContent() : this.jarFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Permission getPermission() throws IOException {
|
||||
return this.jarFileConnection.getPermission();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
if (this.notFound != null) {
|
||||
throwFileNotFound();
|
||||
}
|
||||
if (this.entryName == null) {
|
||||
throw new IOException("no entry name specified");
|
||||
}
|
||||
if (!getUseCaches() && Optimizations.isEnabled(false)) {
|
||||
JarFile cached = jarFiles.getCached(getJarFileURL());
|
||||
if (cached != null) {
|
||||
if (cached.getEntry(this.entryName) != null) {
|
||||
return emptyInputStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
connect();
|
||||
if (this.jarEntry == null) {
|
||||
throwFileNotFound();
|
||||
}
|
||||
return new ConnectionInputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getAllowUserInteraction() {
|
||||
return (this.jarFileConnection != null) ? this.jarFileConnection.getAllowUserInteraction() : false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAllowUserInteraction(boolean allowuserinteraction) {
|
||||
if (this.jarFileConnection != null) {
|
||||
this.jarFileConnection.setAllowUserInteraction(allowuserinteraction);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getUseCaches() {
|
||||
return (this.jarFileConnection != null) ? this.jarFileConnection.getUseCaches() : true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUseCaches(boolean usecaches) {
|
||||
if (this.jarFileConnection != null) {
|
||||
this.jarFileConnection.setUseCaches(usecaches);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getDefaultUseCaches() {
|
||||
return (this.jarFileConnection != null) ? this.jarFileConnection.getDefaultUseCaches() : true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDefaultUseCaches(boolean defaultusecaches) {
|
||||
if (this.jarFileConnection != null) {
|
||||
this.jarFileConnection.setDefaultUseCaches(defaultusecaches);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIfModifiedSince(long ifModifiedSince) {
|
||||
if (this.jarFileConnection != null) {
|
||||
this.jarFileConnection.setIfModifiedSince(ifModifiedSince);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequestProperty(String key) {
|
||||
return (this.jarFileConnection != null) ? this.jarFileConnection.getRequestProperty(key) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequestProperty(String key, String value) {
|
||||
if (this.jarFileConnection != null) {
|
||||
this.jarFileConnection.setRequestProperty(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addRequestProperty(String key, String value) {
|
||||
if (this.jarFileConnection != null) {
|
||||
this.jarFileConnection.addRequestProperty(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getRequestProperties() {
|
||||
return (this.jarFileConnection != null) ? this.jarFileConnection.getRequestProperties()
|
||||
: Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() throws IOException {
|
||||
if (this.connected) {
|
||||
return;
|
||||
}
|
||||
if (this.notFound != null) {
|
||||
throwFileNotFound();
|
||||
}
|
||||
boolean useCaches = getUseCaches();
|
||||
URL jarFileURL = getJarFileURL();
|
||||
if (this.entryName != null && Optimizations.isEnabled()) {
|
||||
assertCachedJarFileHasEntry(jarFileURL, this.entryName);
|
||||
}
|
||||
this.jarFile = jarFiles.getOrCreate(useCaches, jarFileURL);
|
||||
this.jarEntry = getJarEntry(jarFileURL);
|
||||
boolean addedToCache = jarFiles.cacheIfAbsent(useCaches, jarFileURL, this.jarFile);
|
||||
if (addedToCache) {
|
||||
this.jarFileConnection = jarFiles.reconnect(this.jarFile, this.jarFileConnection);
|
||||
}
|
||||
this.connected = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link URLClassLoader} connects often to check if a resource exists, we can
|
||||
* save some object allocations by using the cached copy if we have one.
|
||||
* @param jarFileURL the jar file to check
|
||||
* @param entryName the entry name to check
|
||||
* @throws FileNotFoundException on a missing entry
|
||||
*/
|
||||
private void assertCachedJarFileHasEntry(URL jarFileURL, String entryName) throws FileNotFoundException {
|
||||
JarFile cachedJarFile = jarFiles.getCached(jarFileURL);
|
||||
if (cachedJarFile != null && cachedJarFile.getJarEntry(entryName) == null) {
|
||||
throw FILE_NOT_FOUND_EXCEPTION;
|
||||
}
|
||||
}
|
||||
|
||||
private JarEntry getJarEntry(URL jarFileUrl) throws IOException {
|
||||
if (this.entryName == null) {
|
||||
return null;
|
||||
}
|
||||
JarEntry jarEntry = this.jarFile.getJarEntry(this.entryName);
|
||||
if (jarEntry == null) {
|
||||
jarFiles.closeIfNotCached(jarFileUrl, this.jarFile);
|
||||
throwFileNotFound();
|
||||
}
|
||||
return jarEntry;
|
||||
}
|
||||
|
||||
private void throwFileNotFound() throws FileNotFoundException {
|
||||
if (Optimizations.isEnabled()) {
|
||||
throw FILE_NOT_FOUND_EXCEPTION;
|
||||
}
|
||||
if (this.notFound != null) {
|
||||
throw this.notFound.get();
|
||||
}
|
||||
throw new FileNotFoundException("JAR entry " + this.entryName + " not found in " + this.jarFile.getName());
|
||||
}
|
||||
|
||||
static JarUrlConnection open(URL url) throws IOException {
|
||||
String spec = url.getFile();
|
||||
if (spec.startsWith("nested:")) {
|
||||
int separator = spec.indexOf("!/");
|
||||
boolean specHasEntry = (separator != -1) && (separator + 2 != spec.length());
|
||||
if (specHasEntry) {
|
||||
URL jarFileUrl = new URL(spec.substring(0, separator));
|
||||
if ("runtime".equals(url.getRef())) {
|
||||
jarFileUrl = new URL(jarFileUrl, "#runtime");
|
||||
}
|
||||
String entryName = UrlDecoder.decode(spec.substring(separator + 2));
|
||||
JarFile jarFile = jarFiles.getOrCreate(true, jarFileUrl);
|
||||
jarFiles.cacheIfAbsent(true, jarFileUrl, jarFile);
|
||||
if (!hasEntry(jarFile, entryName)) {
|
||||
return notFoundConnection(jarFile.getName(), entryName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new JarUrlConnection(url);
|
||||
}
|
||||
|
||||
private static boolean hasEntry(JarFile jarFile, String name) {
|
||||
return (jarFile instanceof NestedJarFile nestedJarFile) ? nestedJarFile.hasEntry(name)
|
||||
: jarFile.getEntry(name) != null;
|
||||
}
|
||||
|
||||
private static JarUrlConnection notFoundConnection(String jarFileName, String entryName) throws IOException {
|
||||
if (Optimizations.isEnabled()) {
|
||||
return NOT_FOUND_CONNECTION;
|
||||
}
|
||||
return new JarUrlConnection(
|
||||
() -> new FileNotFoundException("JAR entry " + entryName + " not found in " + jarFileName));
|
||||
}
|
||||
|
||||
static void clearCache() {
|
||||
jarFiles.clearCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection {@link InputStream}. This is not a {@link FilterInputStream} since
|
||||
* {@link URLClassLoader} often creates streams that it doesn't call and we want to be
|
||||
* lazy about getting the underlying {@link InputStream}.
|
||||
*/
|
||||
class ConnectionInputStream extends LazyDelegatingInputStream {
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
super.close();
|
||||
}
|
||||
finally {
|
||||
if (!getUseCaches()) {
|
||||
JarUrlConnection.this.jarFile.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream getDelegateInputStream() throws IOException {
|
||||
return JarUrlConnection.this.jarFile.getInputStream(JarUrlConnection.this.jarEntry);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty {@link URLStreamHandler} used to prevent the wrong JAR Handler from being
|
||||
* Instantiated and cached.
|
||||
*/
|
||||
private static class EmptyUrlStreamHandler extends URLStreamHandler {
|
||||
|
||||
@Override
|
||||
protected URLConnection openConnection(URL url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.protocol.jar;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* {@link InputStream} that delegates lazily to another {@link InputStream}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
abstract class LazyDelegatingInputStream extends InputStream {
|
||||
|
||||
private volatile InputStream in;
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return in().read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b) throws IOException {
|
||||
return in().read(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
return in().read(b, off, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
return in().skip(n);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return in().available();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
try {
|
||||
return in().markSupported();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void mark(int readlimit) {
|
||||
try {
|
||||
in().mark(readlimit);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void reset() throws IOException {
|
||||
in().reset();
|
||||
}
|
||||
|
||||
private InputStream in() throws IOException {
|
||||
InputStream in = this.in;
|
||||
if (in == null) {
|
||||
synchronized (this) {
|
||||
in = this.in;
|
||||
if (in == null) {
|
||||
in = getDelegateInputStream();
|
||||
this.in = in;
|
||||
}
|
||||
}
|
||||
}
|
||||
return in;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
InputStream in = this.in;
|
||||
if (in != null) {
|
||||
synchronized (this) {
|
||||
in = this.in;
|
||||
if (in != null) {
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract InputStream getDelegateInputStream() throws IOException;
|
||||
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.protocol.jar;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
/**
|
||||
* A {@link JarEntry} returned from a {@link UrlJarFile} or {@link UrlNestedJarFile}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class UrlJarEntry extends JarEntry {
|
||||
|
||||
private final UrlJarManifest manifest;
|
||||
|
||||
private UrlJarEntry(JarEntry entry, UrlJarManifest manifest) {
|
||||
super(entry);
|
||||
this.manifest = manifest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Attributes getAttributes() throws IOException {
|
||||
return this.manifest.getEntryAttributes(this);
|
||||
}
|
||||
|
||||
static UrlJarEntry of(ZipEntry entry, UrlJarManifest manifest) {
|
||||
return (entry != null) ? new UrlJarEntry((JarEntry) entry, manifest) : null;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.protocol.jar;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
/**
|
||||
* A {@link JarFile} subclass returned from a {@link JarUrlConnection}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class UrlJarFile extends JarFile {
|
||||
|
||||
private final UrlJarManifest manifest;
|
||||
|
||||
private final Consumer<JarFile> closeAction;
|
||||
|
||||
UrlJarFile(File file, Runtime.Version version, Consumer<JarFile> closeAction) throws IOException {
|
||||
super(file, true, ZipFile.OPEN_READ, version);
|
||||
this.manifest = new UrlJarManifest(super::getManifest);
|
||||
this.closeAction = closeAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZipEntry getEntry(String name) {
|
||||
return UrlJarEntry.of(super.getEntry(name), this.manifest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Manifest getManifest() throws IOException {
|
||||
return this.manifest.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
this.closeAction.accept(this);
|
||||
super.close();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.protocol.jar;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.Runtime.Version;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
import org.springframework.boot.loader.net.protocol.nested.NestedLocation;
|
||||
import org.springframework.boot.loader.net.util.UrlDecoder;
|
||||
|
||||
/**
|
||||
* Factory used by {@link UrlJarFiles} to create {@link JarFile} instances.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @see UrlJarFile
|
||||
* @see UrlNestedJarFile
|
||||
*/
|
||||
class UrlJarFileFactory {
|
||||
|
||||
/**
|
||||
* Create a new {@link UrlJarFile} or {@link UrlNestedJarFile} instance.
|
||||
* @param jarFileUrl the jar file URL
|
||||
* @param closeAction the action to call when the file is closed
|
||||
* @return a new {@link JarFile} instance
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
JarFile createJarFile(URL jarFileUrl, Consumer<JarFile> closeAction) throws IOException {
|
||||
Runtime.Version version = getVersion(jarFileUrl);
|
||||
if (isLocalFileUrl(jarFileUrl)) {
|
||||
return createJarFileForLocalFile(jarFileUrl, version, closeAction);
|
||||
}
|
||||
if (isNestedUrl(jarFileUrl)) {
|
||||
return createJarFileForNested(jarFileUrl, version, closeAction);
|
||||
}
|
||||
return createJarFileForStream(jarFileUrl, version, closeAction);
|
||||
}
|
||||
|
||||
private Runtime.Version getVersion(URL url) {
|
||||
return "runtime".equals(url.getRef()) ? JarFile.runtimeVersion() : JarFile.baseVersion();
|
||||
}
|
||||
|
||||
private boolean isLocalFileUrl(URL url) {
|
||||
return url.getProtocol().equalsIgnoreCase("file") && isLocal(url.getHost());
|
||||
}
|
||||
|
||||
private boolean isLocal(String host) {
|
||||
return host == null || host.isEmpty() || host.equals("~") || host.equalsIgnoreCase("localhost");
|
||||
}
|
||||
|
||||
private JarFile createJarFileForLocalFile(URL url, Runtime.Version version, Consumer<JarFile> closeAction)
|
||||
throws IOException {
|
||||
String path = UrlDecoder.decode(url.getPath());
|
||||
return new UrlJarFile(new File(path), version, closeAction);
|
||||
}
|
||||
|
||||
private boolean isNestedUrl(URL url) {
|
||||
return url.getProtocol().equalsIgnoreCase("nested");
|
||||
}
|
||||
|
||||
private JarFile createJarFileForNested(URL url, Runtime.Version version, Consumer<JarFile> closeAction)
|
||||
throws IOException {
|
||||
NestedLocation location = NestedLocation.fromUrl(url);
|
||||
return new UrlNestedJarFile(location.file(), location.nestedEntryName(), version, closeAction);
|
||||
}
|
||||
|
||||
private JarFile createJarFileForStream(URL url, Version version, Consumer<JarFile> closeAction) throws IOException {
|
||||
try (InputStream in = url.openStream()) {
|
||||
return createJarFileForStream(in, version, closeAction);
|
||||
}
|
||||
}
|
||||
|
||||
private JarFile createJarFileForStream(InputStream in, Version version, Consumer<JarFile> closeAction)
|
||||
throws IOException {
|
||||
Path local = Files.createTempFile("jar_cache", null);
|
||||
try {
|
||||
Files.copy(in, local, StandardCopyOption.REPLACE_EXISTING);
|
||||
JarFile jarFile = new UrlJarFile(local.toFile(), version, closeAction);
|
||||
local.toFile().deleteOnExit();
|
||||
return jarFile;
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
deleteIfPossible(local, ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteIfPossible(Path local, Throwable cause) {
|
||||
try {
|
||||
Files.delete(local);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
cause.addSuppressed(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.protocol.jar;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
/**
|
||||
* Provides access to {@link UrlJarFile} and {@link UrlNestedJarFile} instances taking
|
||||
* care of caching concerns when necessary.
|
||||
* <p>
|
||||
* This class is thread-safe and designed to be shared by all {@link JarUrlConnection}
|
||||
* instances.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class UrlJarFiles {
|
||||
|
||||
private final UrlJarFileFactory factory;
|
||||
|
||||
private final Cache cache = new Cache();
|
||||
|
||||
/**
|
||||
* Create a new {@link UrlJarFiles} instance.
|
||||
*/
|
||||
UrlJarFiles() {
|
||||
this(new UrlJarFileFactory());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link UrlJarFiles} instance.
|
||||
* @param factory the {@link UrlJarFileFactory} to use.
|
||||
*/
|
||||
UrlJarFiles(UrlJarFileFactory factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an existing {@link JarFile} instance from the cache, or create a new
|
||||
* {@link JarFile} instance that can be {@link #cacheIfAbsent(boolean, URL, JarFile)
|
||||
* cached later}.
|
||||
* @param useCaches if caches can be used
|
||||
* @param jarFileUrl the jar file URL
|
||||
* @return a new or existing {@link JarFile} instance
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
JarFile getOrCreate(boolean useCaches, URL jarFileUrl) throws IOException {
|
||||
if (useCaches) {
|
||||
JarFile cached = getCached(jarFileUrl);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
return this.factory.createJarFile(jarFileUrl, this::onClose);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the cached {@link JarFile} if available.
|
||||
* @param jarFileUrl the jar file URL
|
||||
* @return the cached jar or {@code null}
|
||||
*/
|
||||
JarFile getCached(URL jarFileUrl) {
|
||||
return this.cache.get(jarFileUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache the given {@link JarFile} if caching can be used and there is no existing
|
||||
* entry.
|
||||
* @param useCaches if caches can be used
|
||||
* @param jarFileUrl the jar file URL
|
||||
* @param jarFile the jar file
|
||||
* @return {@code true} if that file was added to the cache
|
||||
*/
|
||||
boolean cacheIfAbsent(boolean useCaches, URL jarFileUrl, JarFile jarFile) {
|
||||
if (!useCaches) {
|
||||
return false;
|
||||
}
|
||||
return this.cache.putIfAbsent(jarFileUrl, jarFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the given {@link JarFile} only if it is not contained in the cache.
|
||||
* @param jarFileUrl the jar file URL
|
||||
* @param jarFile the jar file
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
void closeIfNotCached(URL jarFileUrl, JarFile jarFile) throws IOException {
|
||||
JarFile cached = getCached(jarFileUrl);
|
||||
if (cached != jarFile) {
|
||||
jarFile.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnect to the {@link JarFile}, returning a replacement {@link URLConnection}.
|
||||
* @param jarFile the jar file
|
||||
* @param existingConnection the existing connection
|
||||
* @return a newly opened connection inhering the same {@code useCaches} value as the
|
||||
* existing connection
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
URLConnection reconnect(JarFile jarFile, URLConnection existingConnection) throws IOException {
|
||||
Boolean useCaches = (existingConnection != null) ? existingConnection.getUseCaches() : null;
|
||||
URLConnection connection = openConnection(jarFile);
|
||||
if (useCaches != null && connection != null) {
|
||||
connection.setUseCaches(useCaches);
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
private URLConnection openConnection(JarFile jarFile) throws IOException {
|
||||
URL url = this.cache.get(jarFile);
|
||||
return (url != null) ? url.openConnection() : null;
|
||||
}
|
||||
|
||||
private void onClose(JarFile jarFile) {
|
||||
this.cache.remove(jarFile);
|
||||
}
|
||||
|
||||
void clearCache() {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal cache.
|
||||
*/
|
||||
private static class Cache {
|
||||
|
||||
private final Map<String, JarFile> jarFileUrlToJarFile = new HashMap<>();
|
||||
|
||||
private final Map<JarFile, URL> jarFileToJarFileUrl = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Get a {@link JarFile} from the cache given a jar file URL.
|
||||
* @param jarFileUrl the jar file URL
|
||||
* @return the cached {@link JarFile} or {@code null}
|
||||
*/
|
||||
JarFile get(URL jarFileUrl) {
|
||||
String urlKey = JarFileUrlKey.get(jarFileUrl);
|
||||
synchronized (this) {
|
||||
return this.jarFileUrlToJarFile.get(urlKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a jar file URL from the cache given a jar file.
|
||||
* @param jarFile the jar file
|
||||
* @return the cached {@link URL} or {@code null}
|
||||
*/
|
||||
URL get(JarFile jarFile) {
|
||||
synchronized (this) {
|
||||
return this.jarFileToJarFileUrl.get(jarFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Put the given jar file URL and jar file into the cache if they aren't already
|
||||
* there.
|
||||
* @param jarFileUrl the jar file URL
|
||||
* @param jarFile the jar file
|
||||
* @return {@code true} if the items were added to the cache or {@code false} if
|
||||
* they were already there
|
||||
*/
|
||||
boolean putIfAbsent(URL jarFileUrl, JarFile jarFile) {
|
||||
String urlKey = JarFileUrlKey.get(jarFileUrl);
|
||||
synchronized (this) {
|
||||
JarFile cached = this.jarFileUrlToJarFile.get(urlKey);
|
||||
if (cached == null) {
|
||||
this.jarFileUrlToJarFile.put(urlKey, jarFile);
|
||||
this.jarFileToJarFileUrl.put(jarFile, jarFileUrl);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given jar and any related URL file from the cache.
|
||||
* @param jarFile the jar file to remove
|
||||
*/
|
||||
void remove(JarFile jarFile) {
|
||||
synchronized (this) {
|
||||
URL removedUrl = this.jarFileToJarFileUrl.remove(jarFile);
|
||||
if (removedUrl != null) {
|
||||
this.jarFileUrlToJarFile.remove(JarFileUrlKey.get(removedUrl));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
synchronized (this) {
|
||||
this.jarFileToJarFileUrl.clear();
|
||||
this.jarFileUrlToJarFile.clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.protocol.jar;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
/**
|
||||
* Provides access {@link Manifest} content that can be safely returned from
|
||||
* {@link UrlJarFile} or {@link UrlNestedJarFile}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class UrlJarManifest {
|
||||
|
||||
private static final Object NONE = new Object();
|
||||
|
||||
private final ManifestSupplier supplier;
|
||||
|
||||
private volatile Object supplied;
|
||||
|
||||
UrlJarManifest(ManifestSupplier supplier) {
|
||||
this.supplier = supplier;
|
||||
}
|
||||
|
||||
Manifest get() throws IOException {
|
||||
Manifest manifest = supply();
|
||||
if (manifest == null) {
|
||||
return null;
|
||||
}
|
||||
Manifest copy = new Manifest();
|
||||
copy.getMainAttributes().putAll((Map<?, ?>) manifest.getMainAttributes().clone());
|
||||
manifest.getEntries().forEach((key, value) -> copy.getEntries().put(key, cloneAttributes(value)));
|
||||
return copy;
|
||||
}
|
||||
|
||||
Attributes getEntryAttributes(JarEntry entry) throws IOException {
|
||||
Manifest manifest = supply();
|
||||
if (manifest == null) {
|
||||
return null;
|
||||
}
|
||||
Attributes attributes = manifest.getEntries().get(entry.getName());
|
||||
return cloneAttributes(attributes);
|
||||
}
|
||||
|
||||
private Attributes cloneAttributes(Attributes attributes) {
|
||||
return (attributes != null) ? (Attributes) attributes.clone() : null;
|
||||
}
|
||||
|
||||
private Manifest supply() throws IOException {
|
||||
Object supplied = this.supplied;
|
||||
if (supplied == null) {
|
||||
supplied = this.supplier.getManifest();
|
||||
this.supplied = (supplied != null) ? supplied : NONE;
|
||||
}
|
||||
return (supplied != NONE) ? (Manifest) supplied : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface used to supply the actual manifest.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface ManifestSupplier {
|
||||
|
||||
Manifest getManifest() throws IOException;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.protocol.jar;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.Runtime.Version;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.springframework.boot.loader.jar.NestedJarFile;
|
||||
|
||||
/**
|
||||
* {@link NestedJarFile} subclass returned from a {@link JarUrlConnection}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class UrlNestedJarFile extends NestedJarFile {
|
||||
|
||||
private final UrlJarManifest manifest;
|
||||
|
||||
private final Consumer<JarFile> closeAction;
|
||||
|
||||
UrlNestedJarFile(File file, String nestedEntryName, Version version, Consumer<JarFile> closeAction)
|
||||
throws IOException {
|
||||
super(file, nestedEntryName, version);
|
||||
this.manifest = new UrlJarManifest(super::getManifest);
|
||||
this.closeAction = closeAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Manifest getManifest() throws IOException {
|
||||
return this.manifest.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JarEntry getEntry(String name) {
|
||||
return UrlJarEntry.of(super.getEntry(name), this.manifest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
this.closeAction.accept(this);
|
||||
super.close();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* JAR URL support, including support for nested jars.
|
||||
*
|
||||
* @see org.springframework.boot.loader.net.protocol.jar.JarUrl
|
||||
* @see org.springframework.boot.loader.net.protocol.jar.Handler
|
||||
*/
|
||||
package org.springframework.boot.loader.net.protocol.jar;
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.protocol.nested;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
|
||||
/**
|
||||
* {@link URLStreamHandler} to support {@code nested:} URLs. See {@link NestedLocation}
|
||||
* for details of the URL format.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.2.0
|
||||
*/
|
||||
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 '.nested'
|
||||
|
||||
private static final String PREFIX = "nested:";
|
||||
|
||||
@Override
|
||||
protected URLConnection openConnection(URL url) throws IOException {
|
||||
return new NestedUrlConnection(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the specified URL is a valid "nested" URL.
|
||||
* @param url the URL to check
|
||||
*/
|
||||
public static void assertUrlIsNotMalformed(String url) {
|
||||
if (url == null || !url.startsWith(PREFIX)) {
|
||||
throw new IllegalArgumentException("'url' must not be null and must use 'nested' protocol");
|
||||
}
|
||||
NestedLocation.parse(url.substring(PREFIX.length()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any internal caches.
|
||||
*/
|
||||
public static void clearCache() {
|
||||
NestedLocation.clearCache();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.protocol.nested;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.boot.loader.net.util.UrlDecoder;
|
||||
|
||||
/**
|
||||
* A location obtained from a {@code nested:} {@link URL} consisting of a jar file and a
|
||||
* nested entry.
|
||||
* <p>
|
||||
* The syntax of a nested JAR URL is: <pre>
|
||||
* nestedjar:<path>/!{entry}
|
||||
* </pre>
|
||||
* <p>
|
||||
* for example:
|
||||
* <p>
|
||||
* {@code nested:/home/example/my.jar/!BOOT-INF/lib/my-nested.jar}
|
||||
* <p>
|
||||
* or:
|
||||
* <p>
|
||||
* {@code nested:/home/example/my.jar/!BOOT-INF/classes/}
|
||||
* <p>
|
||||
* The path must refer to a jar file on the file system. The entry refers to either an
|
||||
* uncompressed entry that contains the nested jar, or a directory entry. The entry must
|
||||
* not start with a {@code '/'}.
|
||||
*
|
||||
* @param file the zip file that contains the nested entry
|
||||
* @param nestedEntryName the nested entry name
|
||||
* @author Phillip Webb
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public record NestedLocation(File file, String nestedEntryName) {
|
||||
|
||||
private static final Map<String, NestedLocation> cache = new ConcurrentHashMap<>();
|
||||
|
||||
public NestedLocation {
|
||||
if (file == null) {
|
||||
throw new IllegalArgumentException("'file' must not be null");
|
||||
}
|
||||
if (nestedEntryName == null || nestedEntryName.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("'nestedEntryName' must not be empty");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link NestedLocation} from the given URL.
|
||||
* @param url the nested URL
|
||||
* @return a new {@link NestedLocation} instance
|
||||
* @throws IllegalArgumentException if the URL is not valid
|
||||
*/
|
||||
public static NestedLocation fromUrl(URL url) {
|
||||
if (url == null || !"nested".equalsIgnoreCase(url.getProtocol())) {
|
||||
throw new IllegalArgumentException("'url' must not be null and must use 'nested' protocol");
|
||||
}
|
||||
return parse(UrlDecoder.decode(url.getPath()));
|
||||
}
|
||||
|
||||
static NestedLocation parse(String path) {
|
||||
if (path == null || path.isEmpty()) {
|
||||
throw new IllegalArgumentException("'path' must not be empty");
|
||||
}
|
||||
int index = path.lastIndexOf("/!");
|
||||
if (index == -1) {
|
||||
throw new IllegalArgumentException("'path' must contain '/!'");
|
||||
}
|
||||
return cache.computeIfAbsent(path, (l) -> create(index, l));
|
||||
}
|
||||
|
||||
private static NestedLocation create(int index, String location) {
|
||||
String file = location.substring(0, index);
|
||||
String nestedEntryName = location.substring(index + 2);
|
||||
return new NestedLocation((!file.isEmpty()) ? new File(file) : null, nestedEntryName);
|
||||
}
|
||||
|
||||
static void clearCache() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.protocol.nested;
|
||||
|
||||
import java.io.FilePermission;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.ref.Cleaner.Cleanable;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.security.Permission;
|
||||
|
||||
import org.springframework.boot.loader.ref.Cleaner;
|
||||
|
||||
/**
|
||||
* {@link URLConnection} to support {@code nested:} URLs. See {@link NestedLocation} for
|
||||
* details of the URL format.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class NestedUrlConnection extends URLConnection {
|
||||
|
||||
private static final String CONTENT_TYPE = "x-java/jar";
|
||||
|
||||
private final NestedUrlConnectionResources resources;
|
||||
|
||||
private final Cleanable cleanup;
|
||||
|
||||
private long lastModified;
|
||||
|
||||
private FilePermission permission;
|
||||
|
||||
NestedUrlConnection(URL url) throws MalformedURLException {
|
||||
this(url, Cleaner.instance);
|
||||
}
|
||||
|
||||
NestedUrlConnection(URL url, Cleaner cleaner) throws MalformedURLException {
|
||||
super(url);
|
||||
NestedLocation location = parseNestedLocation(url);
|
||||
this.resources = new NestedUrlConnectionResources(location);
|
||||
this.cleanup = cleaner.register(this, this.resources);
|
||||
}
|
||||
|
||||
private NestedLocation parseNestedLocation(URL url) throws MalformedURLException {
|
||||
try {
|
||||
return NestedLocation.parse(url.getPath());
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
throw new MalformedURLException(ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getContentLength() {
|
||||
long contentLength = getContentLengthLong();
|
||||
return (contentLength <= Integer.MAX_VALUE) ? (int) contentLength : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentLengthLong() {
|
||||
try {
|
||||
connect();
|
||||
return this.resources.getContentLength();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return CONTENT_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLastModified() {
|
||||
if (this.lastModified == 0) {
|
||||
this.lastModified = this.resources.getLocation().file().lastModified();
|
||||
}
|
||||
return this.lastModified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Permission getPermission() throws IOException {
|
||||
if (this.permission == null) {
|
||||
this.permission = new FilePermission(this.resources.getLocation().file().getCanonicalPath(), "read");
|
||||
}
|
||||
return this.permission;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
connect();
|
||||
return new ConnectionInputStream(this.resources.getInputStream());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() throws IOException {
|
||||
if (this.connected) {
|
||||
return;
|
||||
}
|
||||
this.resources.connect();
|
||||
this.connected = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection {@link InputStream}.
|
||||
*/
|
||||
class ConnectionInputStream extends FilterInputStream {
|
||||
|
||||
private volatile boolean closing;
|
||||
|
||||
ConnectionInputStream(InputStream in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (this.closing) {
|
||||
return;
|
||||
}
|
||||
this.closing = true;
|
||||
try {
|
||||
super.close();
|
||||
}
|
||||
finally {
|
||||
try {
|
||||
NestedUrlConnection.this.cleanup.clean();
|
||||
}
|
||||
catch (UncheckedIOException ex) {
|
||||
throw ex.getCause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.protocol.nested;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
import org.springframework.boot.loader.ref.Cleaner;
|
||||
import org.springframework.boot.loader.zip.CloseableDataBlock;
|
||||
import org.springframework.boot.loader.zip.ZipContent;
|
||||
|
||||
/**
|
||||
* Resources created managed and cleaned by a {@link NestedUrlConnection} instance and
|
||||
* suitable for registration with a {@link Cleaner}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class NestedUrlConnectionResources implements Runnable {
|
||||
|
||||
private final NestedLocation location;
|
||||
|
||||
private volatile ZipContent zipContent;
|
||||
|
||||
private volatile long size = -1;
|
||||
|
||||
private volatile InputStream inputStream;
|
||||
|
||||
NestedUrlConnectionResources(NestedLocation location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
NestedLocation getLocation() {
|
||||
return this.location;
|
||||
}
|
||||
|
||||
void connect() throws IOException {
|
||||
synchronized (this) {
|
||||
if (this.zipContent == null) {
|
||||
this.zipContent = ZipContent.open(this.location.file().toPath(), this.location.nestedEntryName());
|
||||
try {
|
||||
connectData();
|
||||
}
|
||||
catch (IOException | RuntimeException ex) {
|
||||
this.zipContent.close();
|
||||
this.zipContent = null;
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void connectData() throws IOException {
|
||||
CloseableDataBlock data = this.zipContent.openRawZipData();
|
||||
try {
|
||||
this.size = data.size();
|
||||
this.inputStream = data.asInputStream();
|
||||
}
|
||||
catch (IOException | RuntimeException ex) {
|
||||
data.close();
|
||||
}
|
||||
}
|
||||
|
||||
InputStream getInputStream() throws IOException {
|
||||
synchronized (this) {
|
||||
if (this.inputStream == null) {
|
||||
throw new IOException("Nested location not found " + this.location);
|
||||
}
|
||||
return this.inputStream;
|
||||
}
|
||||
}
|
||||
|
||||
long getContentLength() {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
releaseAll();
|
||||
}
|
||||
|
||||
private void releaseAll() {
|
||||
synchronized (this) {
|
||||
if (this.zipContent != null) {
|
||||
IOException exceptionChain = null;
|
||||
try {
|
||||
this.inputStream.close();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
exceptionChain = addToExceptionChain(exceptionChain, ex);
|
||||
}
|
||||
try {
|
||||
this.zipContent.close();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
exceptionChain = addToExceptionChain(exceptionChain, ex);
|
||||
}
|
||||
this.size = -1;
|
||||
if (exceptionChain != null) {
|
||||
throw new UncheckedIOException(exceptionChain);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IOException addToExceptionChain(IOException exceptionChain, IOException ex) {
|
||||
if (exceptionChain != null) {
|
||||
exceptionChain.addSuppressed(ex);
|
||||
return exceptionChain;
|
||||
}
|
||||
return ex;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Nested URL support.
|
||||
*
|
||||
* @see org.springframework.boot.loader.net.protocol.nested.NestedLocation
|
||||
* @see org.springframework.boot.loader.net.protocol.nested.Handler
|
||||
*/
|
||||
package org.springframework.boot.loader.net.protocol.nested;
|
@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.net.util;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.CoderResult;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Utility to decode URL strings.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public final class UrlDecoder {
|
||||
|
||||
private UrlDecoder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the given string by decoding URL {@code '%'} escapes. This method should be
|
||||
* identical in behavior to the {@code decode} method in the internal
|
||||
* {@code sun.net.www.ParseUtil} JDK class.
|
||||
* @param string the string to decode
|
||||
* @return the decoded string
|
||||
*/
|
||||
public static String decode(String string) {
|
||||
int length = string.length();
|
||||
if ((length == 0) || (string.indexOf('%') < 0)) {
|
||||
return string;
|
||||
}
|
||||
StringBuilder result = new StringBuilder(length);
|
||||
ByteBuffer byteBuffer = ByteBuffer.allocate(length);
|
||||
CharBuffer charBuffer = CharBuffer.allocate(length);
|
||||
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
|
||||
.onMalformedInput(CodingErrorAction.REPORT)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPORT);
|
||||
int index = 0;
|
||||
while (index < length) {
|
||||
char ch = string.charAt(index);
|
||||
if (ch != '%') {
|
||||
result.append(ch);
|
||||
if (index + 1 >= length) {
|
||||
return result.toString();
|
||||
}
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
index = fillByteBuffer(byteBuffer, string, index, length);
|
||||
decodeToCharBuffer(byteBuffer, charBuffer, decoder);
|
||||
result.append(charBuffer.flip());
|
||||
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private static int fillByteBuffer(ByteBuffer byteBuffer, String string, int index, int length) {
|
||||
byteBuffer.clear();
|
||||
while (true) {
|
||||
byteBuffer.put(unescape(string, index));
|
||||
index += 3;
|
||||
if (index >= length || string.charAt(index) != '%') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
byteBuffer.flip();
|
||||
return index;
|
||||
}
|
||||
|
||||
private static byte unescape(String string, int index) {
|
||||
try {
|
||||
return (byte) Integer.parseInt(string, index + 1, index + 3, 16);
|
||||
}
|
||||
catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void decodeToCharBuffer(ByteBuffer byteBuffer, CharBuffer charBuffer, CharsetDecoder decoder) {
|
||||
decoder.reset();
|
||||
charBuffer.clear();
|
||||
assertNoError(decoder.decode(byteBuffer, charBuffer, true));
|
||||
assertNoError(decoder.flush(charBuffer));
|
||||
}
|
||||
|
||||
private static void assertNoError(CoderResult result) {
|
||||
if (result.isError()) {
|
||||
throw new IllegalArgumentException("Error decoding percent encoded characters");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* System that allows self-contained JAR/WAR archives to be launched using
|
||||
* {@code java -jar}. Archives can include nested packaged dependency JARs (there is no
|
||||
* need to create shade style jars) and are executed without unpacking. The only
|
||||
* constraint is that nested JARs must be stored in the archive uncompressed.
|
||||
*
|
||||
* @see org.springframework.boot.loader.JarLauncher
|
||||
* @see org.springframework.boot.loader.WarLauncher
|
||||
*/
|
||||
package org.springframework.boot.loader;
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.ref;
|
||||
|
||||
import java.lang.ref.Cleaner.Cleanable;
|
||||
|
||||
/**
|
||||
* Wrapper for {@link java.lang.ref.Cleaner} providing registration support.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public interface Cleaner {
|
||||
|
||||
/**
|
||||
* Provides access to the default clean instance which delegates to
|
||||
* {@link java.lang.ref.Cleaner}.
|
||||
*/
|
||||
Cleaner instance = DefaultCleaner.instance;
|
||||
|
||||
/**
|
||||
* Registers an object and the clean action to run when the object becomes phantom
|
||||
* reachable.
|
||||
* @param obj the object to monitor
|
||||
* @param action the cleanup action to run
|
||||
* @return a {@link Cleanable} instance
|
||||
* @see java.lang.ref.Cleaner#register(Object, Runnable)
|
||||
*/
|
||||
Cleanable register(Object obj, Runnable action);
|
||||
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.ref;
|
||||
|
||||
import java.lang.ref.Cleaner.Cleanable;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Default {@link Cleaner} implementation that delegates to {@link java.lang.ref.Cleaner}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class DefaultCleaner implements Cleaner {
|
||||
|
||||
static final DefaultCleaner instance = new DefaultCleaner();
|
||||
|
||||
static Consumer<Cleanable> tracker;
|
||||
|
||||
private final java.lang.ref.Cleaner cleaner = java.lang.ref.Cleaner.create();
|
||||
|
||||
@Override
|
||||
public Cleanable register(Object obj, Runnable action) {
|
||||
Cleanable cleanable = this.cleaner.register(obj, action);
|
||||
if (tracker != null) {
|
||||
tracker.accept(cleanable);
|
||||
}
|
||||
return cleanable;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Support for {@link java.lang.ref.Cleaner}.
|
||||
*/
|
||||
package org.springframework.boot.loader.ref;
|
@ -1,232 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.util;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Helper class for resolving placeholders in texts. Usually applied to file paths.
|
||||
* <p>
|
||||
* A text may contain {@code $ ...} placeholders, to be resolved as system properties:
|
||||
* e.g. {@code $ user.dir}. Default values can be supplied using the ":" separator between
|
||||
* key and value.
|
||||
* <p>
|
||||
* Adapted from Spring.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Rob Harrop
|
||||
* @author Dave Syer
|
||||
* @since 1.0.0
|
||||
* @see System#getProperty(String)
|
||||
*/
|
||||
public abstract class SystemPropertyUtils {
|
||||
|
||||
/**
|
||||
* Prefix for system property placeholders: "${".
|
||||
*/
|
||||
public static final String PLACEHOLDER_PREFIX = "${";
|
||||
|
||||
/**
|
||||
* Suffix for system property placeholders: "}".
|
||||
*/
|
||||
public static final String PLACEHOLDER_SUFFIX = "}";
|
||||
|
||||
/**
|
||||
* Value separator for system property placeholders: ":".
|
||||
*/
|
||||
public static final String VALUE_SEPARATOR = ":";
|
||||
|
||||
private static final String SIMPLE_PREFIX = PLACEHOLDER_PREFIX.substring(1);
|
||||
|
||||
/**
|
||||
* Resolve ${...} placeholders in the given text, replacing them with corresponding
|
||||
* system property values.
|
||||
* @param text the String to resolve
|
||||
* @return the resolved String
|
||||
* @throws IllegalArgumentException if there is an unresolvable placeholder
|
||||
* @see #PLACEHOLDER_PREFIX
|
||||
* @see #PLACEHOLDER_SUFFIX
|
||||
*/
|
||||
public static String resolvePlaceholders(String text) {
|
||||
if (text == null) {
|
||||
return text;
|
||||
}
|
||||
return parseStringValue(null, text, text, new HashSet<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve ${...} placeholders in the given text, replacing them with corresponding
|
||||
* system property values.
|
||||
* @param properties a properties instance to use in addition to System
|
||||
* @param text the String to resolve
|
||||
* @return the resolved String
|
||||
* @throws IllegalArgumentException if there is an unresolvable placeholder
|
||||
* @see #PLACEHOLDER_PREFIX
|
||||
* @see #PLACEHOLDER_SUFFIX
|
||||
*/
|
||||
public static String resolvePlaceholders(Properties properties, String text) {
|
||||
if (text == null) {
|
||||
return text;
|
||||
}
|
||||
return parseStringValue(properties, text, text, new HashSet<>());
|
||||
}
|
||||
|
||||
private static String parseStringValue(Properties properties, String value, String current,
|
||||
Set<String> visitedPlaceholders) {
|
||||
|
||||
StringBuilder buf = new StringBuilder(current);
|
||||
|
||||
int startIndex = current.indexOf(PLACEHOLDER_PREFIX);
|
||||
while (startIndex != -1) {
|
||||
int endIndex = findPlaceholderEndIndex(buf, startIndex);
|
||||
if (endIndex != -1) {
|
||||
String placeholder = buf.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex);
|
||||
String originalPlaceholder = placeholder;
|
||||
if (!visitedPlaceholders.add(originalPlaceholder)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
|
||||
}
|
||||
// Recursive invocation, parsing placeholders contained in the
|
||||
// placeholder
|
||||
// key.
|
||||
placeholder = parseStringValue(properties, value, placeholder, visitedPlaceholders);
|
||||
// Now obtain the value for the fully resolved key...
|
||||
String propVal = resolvePlaceholder(properties, value, placeholder);
|
||||
if (propVal == null) {
|
||||
int separatorIndex = placeholder.indexOf(VALUE_SEPARATOR);
|
||||
if (separatorIndex != -1) {
|
||||
String actualPlaceholder = placeholder.substring(0, separatorIndex);
|
||||
String defaultValue = placeholder.substring(separatorIndex + VALUE_SEPARATOR.length());
|
||||
propVal = resolvePlaceholder(properties, value, actualPlaceholder);
|
||||
if (propVal == null) {
|
||||
propVal = defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (propVal != null) {
|
||||
// Recursive invocation, parsing placeholders contained in the
|
||||
// previously resolved placeholder value.
|
||||
propVal = parseStringValue(properties, value, propVal, visitedPlaceholders);
|
||||
buf.replace(startIndex, endIndex + PLACEHOLDER_SUFFIX.length(), propVal);
|
||||
startIndex = buf.indexOf(PLACEHOLDER_PREFIX, startIndex + propVal.length());
|
||||
}
|
||||
else {
|
||||
// Proceed with unprocessed value.
|
||||
startIndex = buf.indexOf(PLACEHOLDER_PREFIX, endIndex + PLACEHOLDER_SUFFIX.length());
|
||||
}
|
||||
visitedPlaceholders.remove(originalPlaceholder);
|
||||
}
|
||||
else {
|
||||
startIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
private static String resolvePlaceholder(Properties properties, String text, String placeholderName) {
|
||||
String propVal = getProperty(placeholderName, null, text);
|
||||
if (propVal != null) {
|
||||
return propVal;
|
||||
}
|
||||
return (properties != null) ? properties.getProperty(placeholderName) : null;
|
||||
}
|
||||
|
||||
public static String getProperty(String key) {
|
||||
return getProperty(key, null, "");
|
||||
}
|
||||
|
||||
public static String getProperty(String key, String defaultValue) {
|
||||
return getProperty(key, defaultValue, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the System properties and environment variables for a value with the
|
||||
* provided key. Environment variables in {@code UPPER_CASE} style are allowed where
|
||||
* System properties would normally be {@code lower.case}.
|
||||
* @param key the key to resolve
|
||||
* @param defaultValue the default value
|
||||
* @param text optional extra context for an error message if the key resolution fails
|
||||
* (e.g. if System properties are not accessible)
|
||||
* @return a static property value or null of not found
|
||||
*/
|
||||
public static String getProperty(String key, String defaultValue, String text) {
|
||||
try {
|
||||
String propVal = System.getProperty(key);
|
||||
if (propVal == null) {
|
||||
// Fall back to searching the system environment.
|
||||
propVal = System.getenv(key);
|
||||
}
|
||||
if (propVal == null) {
|
||||
// Try with underscores.
|
||||
String name = key.replace('.', '_');
|
||||
propVal = System.getenv(name);
|
||||
}
|
||||
if (propVal == null) {
|
||||
// Try uppercase with underscores as well.
|
||||
String name = key.toUpperCase(Locale.ENGLISH).replace('.', '_');
|
||||
propVal = System.getenv(name);
|
||||
}
|
||||
if (propVal != null) {
|
||||
return propVal;
|
||||
}
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
System.err.println("Could not resolve key '" + key + "' in '" + text
|
||||
+ "' as system property or in environment: " + ex);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private static int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
|
||||
int index = startIndex + PLACEHOLDER_PREFIX.length();
|
||||
int withinNestedPlaceholder = 0;
|
||||
while (index < buf.length()) {
|
||||
if (substringMatch(buf, index, PLACEHOLDER_SUFFIX)) {
|
||||
if (withinNestedPlaceholder > 0) {
|
||||
withinNestedPlaceholder--;
|
||||
index = index + PLACEHOLDER_SUFFIX.length();
|
||||
}
|
||||
else {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
else if (substringMatch(buf, index, SIMPLE_PREFIX)) {
|
||||
withinNestedPlaceholder++;
|
||||
index = index + SIMPLE_PREFIX.length();
|
||||
}
|
||||
else {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static boolean substringMatch(CharSequence str, int index, CharSequence substring) {
|
||||
for (int j = 0; j < substring.length(); j++) {
|
||||
int i = index + j;
|
||||
if (i >= str.length() || str.charAt(i) != substring.charAt(j)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.zip;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* {@link DataBlock} backed by a byte array .
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ByteArrayDataBlock implements DataBlock {
|
||||
|
||||
private final byte[] bytes;
|
||||
|
||||
/**
|
||||
* Create a new {@link ByteArrayDataBlock} backed by the given bytes.
|
||||
* @param bytes the bytes to use
|
||||
*/
|
||||
ByteArrayDataBlock(byte... bytes) {
|
||||
this.bytes = bytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws IOException {
|
||||
return this.bytes.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer dst, long pos) throws IOException {
|
||||
return read(dst, (int) pos);
|
||||
}
|
||||
|
||||
private int read(ByteBuffer dst, int pos) {
|
||||
int remaining = dst.remaining();
|
||||
int length = Math.min(this.bytes.length - pos, remaining);
|
||||
dst.put(this.bytes, pos, length);
|
||||
return length;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.zip;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
/**
|
||||
* Provides read access to a block of data contained somewhere in a zip file.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public interface DataBlock {
|
||||
|
||||
/**
|
||||
* Return the size of this block.
|
||||
* @return the block size
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
long size() throws IOException;
|
||||
|
||||
/**
|
||||
* Read a sequence of bytes from this channel into the given buffer, starting at the
|
||||
* given block position.
|
||||
* @param dst the buffer into which bytes are to be transferred
|
||||
* @param pos the position within the block at which the transfer is to begin
|
||||
* @return the number of bytes read, possibly zero, or {@code -1} if the given
|
||||
* position is greater than or equal to the block size
|
||||
* @throws IOException on I/O error
|
||||
* @see #readFully(ByteBuffer, long)
|
||||
* @see FileChannel#read(ByteBuffer, long)
|
||||
*/
|
||||
int read(ByteBuffer dst, long pos) throws IOException;
|
||||
|
||||
/**
|
||||
* Fully read a sequence of bytes from this channel into the given buffer, starting at
|
||||
* the given block position and filling {@link ByteBuffer#remaining() remaining} bytes
|
||||
* in the buffer.
|
||||
* @param dst the buffer into which bytes are to be transferred
|
||||
* @param pos the position within the block at which the transfer is to begin
|
||||
* @throws EOFException if an attempt is made to read past the end of the block
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
default void readFully(ByteBuffer dst, long pos) throws IOException {
|
||||
do {
|
||||
int count = read(dst, pos);
|
||||
if (count <= 0) {
|
||||
throw new EOFException();
|
||||
}
|
||||
pos += count;
|
||||
}
|
||||
while (dst.hasRemaining());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return this {@link DataBlock} as an {@link InputStream}.
|
||||
* @return an {@link InputStream} to read the data block content
|
||||
*/
|
||||
default InputStream asInputStream() {
|
||||
return new DataBlockInputStream(this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.zip;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.zip.ZipException;
|
||||
|
||||
/**
|
||||
* {@link InputStream} backed by a {@link DataBlock}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class DataBlockInputStream extends InputStream {
|
||||
|
||||
private final DataBlock dataBlock;
|
||||
|
||||
private long pos;
|
||||
|
||||
private long remaining;
|
||||
|
||||
private volatile boolean closing;
|
||||
|
||||
DataBlockInputStream(DataBlock dataBlock) {
|
||||
this.dataBlock = dataBlock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
byte[] b = new byte[1];
|
||||
return (read(b, 0, 1) == 1) ? b[0] & 0xFF : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int result;
|
||||
ensureOpen();
|
||||
ByteBuffer dst = ByteBuffer.wrap(b, off, len);
|
||||
int count = this.dataBlock.read(dst, this.pos);
|
||||
if (count > 0) {
|
||||
this.pos += count;
|
||||
this.remaining -= count;
|
||||
}
|
||||
result = count;
|
||||
if (this.remaining == 0) {
|
||||
close();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
long result;
|
||||
result = (n > 0) ? maxForwardSkip(n) : maxBackwardSkip(n);
|
||||
this.pos += result;
|
||||
this.remaining -= result;
|
||||
if (this.remaining == 0) {
|
||||
close();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private long maxForwardSkip(long n) {
|
||||
boolean willCauseOverflow = (this.pos + n) < 0;
|
||||
return (willCauseOverflow || n > this.remaining) ? this.remaining : n;
|
||||
}
|
||||
|
||||
private long maxBackwardSkip(long n) {
|
||||
return Math.max(-this.pos, n);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
return (this.remaining < Integer.MAX_VALUE) ? (int) this.remaining : Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
private void ensureOpen() throws ZipException {
|
||||
if (this.closing) {
|
||||
throw new ZipException("InputStream closed");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (this.closing) {
|
||||
return;
|
||||
}
|
||||
this.closing = true;
|
||||
if (this.dataBlock instanceof Closeable closeable) {
|
||||
closeable.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.zip;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.boot.loader.log.DebugLogger;
|
||||
|
||||
/**
|
||||
* Reference counted {@link DataBlock} implementation backed by a {@link FileChannel} with
|
||||
* support for slicing.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class FileChannelDataBlock implements CloseableDataBlock {
|
||||
|
||||
private static final DebugLogger debug = DebugLogger.get(FileChannelDataBlock.class);
|
||||
|
||||
static Tracker tracker;
|
||||
|
||||
private final ManagedFileChannel channel;
|
||||
|
||||
private final long offset;
|
||||
|
||||
private final long size;
|
||||
|
||||
FileChannelDataBlock(Path path) throws IOException {
|
||||
this.channel = new ManagedFileChannel(path);
|
||||
this.offset = 0;
|
||||
this.size = Files.size(path);
|
||||
}
|
||||
|
||||
FileChannelDataBlock(ManagedFileChannel channel, long offset, long size) {
|
||||
this.channel = channel;
|
||||
this.offset = offset;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws IOException {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer dst, long pos) throws IOException {
|
||||
if (pos < 0) {
|
||||
throw new IllegalArgumentException("Position must not be negative");
|
||||
}
|
||||
ensureOpen(ClosedChannelException::new);
|
||||
int remaining = (int) (this.size - pos);
|
||||
if (remaining <= 0) {
|
||||
return -1;
|
||||
}
|
||||
int originalDestinationLimit = -1;
|
||||
if (dst.remaining() > remaining) {
|
||||
originalDestinationLimit = dst.limit();
|
||||
dst.limit(dst.position() + remaining);
|
||||
}
|
||||
int result = this.channel.read(dst, this.offset + pos);
|
||||
if (originalDestinationLimit != -1) {
|
||||
dst.limit(originalDestinationLimit);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a connection to this block, increasing the reference count and re-opening the
|
||||
* underlying file channel if necessary.
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
void open() throws IOException {
|
||||
this.channel.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a connection to this block, decreasing the reference count and closing the
|
||||
* underlying file channel if necessary.
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
this.channel.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the underlying file channel is currently open.
|
||||
* @param exceptionSupplier a supplier providing the exception to throw
|
||||
* @param <E> the exception type
|
||||
* @throws E if the channel is closed
|
||||
*/
|
||||
<E extends Exception> void ensureOpen(Supplier<E> exceptionSupplier) throws E {
|
||||
this.channel.ensureOpen(exceptionSupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link FileChannelDataBlock} slice providing access to a subset of the
|
||||
* data. The caller is responsible for calling {@link #open()} and {@link #close()} on
|
||||
* the returned block.
|
||||
* @param offset the start offset for the slice relative to this block
|
||||
* @return a new {@link FileChannelDataBlock} instance
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
FileChannelDataBlock slice(long offset) throws IOException {
|
||||
return slice(offset, this.size - offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link FileChannelDataBlock} slice providing access to a subset of the
|
||||
* data. The caller is responsible for calling {@link #open()} and {@link #close()} on
|
||||
* the returned block.
|
||||
* @param offset the start offset for the slice relative to this block
|
||||
* @param size the size of the new slice
|
||||
* @return a new {@link FileChannelDataBlock} instance
|
||||
*/
|
||||
FileChannelDataBlock slice(long offset, long size) {
|
||||
if (offset == 0 && size == this.size) {
|
||||
return this;
|
||||
}
|
||||
if (offset < 0) {
|
||||
throw new IllegalArgumentException("Offset must not be negative");
|
||||
}
|
||||
if (size < 0 || offset + size > this.size) {
|
||||
throw new IllegalArgumentException("Size must not be negative and must be within bounds");
|
||||
}
|
||||
debug.log("Slicing %s at %s with size %s", this.channel, offset, size);
|
||||
return new FileChannelDataBlock(this.channel, this.offset + offset, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages access to underlying {@link FileChannel}.
|
||||
*/
|
||||
static class ManagedFileChannel {
|
||||
|
||||
static final int BUFFER_SIZE = 1024 * 10;
|
||||
|
||||
private final Path path;
|
||||
|
||||
private int referenceCount;
|
||||
|
||||
private FileChannel fileChannel;
|
||||
|
||||
private ByteBuffer buffer;
|
||||
|
||||
private long bufferPosition = -1;
|
||||
|
||||
private int bufferSize;
|
||||
|
||||
private final Object lock = new Object();
|
||||
|
||||
ManagedFileChannel(Path path) {
|
||||
if (!Files.isRegularFile(path)) {
|
||||
throw new IllegalArgumentException(path + " must be a regular file");
|
||||
}
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
int read(ByteBuffer dst, long position) throws IOException {
|
||||
synchronized (this.lock) {
|
||||
if (position < this.bufferPosition || position >= this.bufferPosition + this.bufferSize) {
|
||||
this.buffer.clear();
|
||||
this.bufferSize = this.fileChannel.read(this.buffer, position);
|
||||
this.bufferPosition = position;
|
||||
}
|
||||
if (this.bufferSize <= 0) {
|
||||
return this.bufferSize;
|
||||
}
|
||||
int offset = (int) (position - this.bufferPosition);
|
||||
int length = Math.min(this.bufferSize - offset, dst.remaining());
|
||||
dst.put(dst.position(), this.buffer, offset, length);
|
||||
dst.position(dst.position() + length);
|
||||
return length;
|
||||
}
|
||||
}
|
||||
|
||||
void open() throws IOException {
|
||||
synchronized (this.lock) {
|
||||
if (this.referenceCount == 0) {
|
||||
debug.log("Opening '%s'", this.path);
|
||||
this.fileChannel = FileChannel.open(this.path, StandardOpenOption.READ);
|
||||
this.buffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
|
||||
if (tracker != null) {
|
||||
tracker.openedFileChannel(this.path, this.fileChannel);
|
||||
}
|
||||
}
|
||||
this.referenceCount++;
|
||||
debug.log("Reference count for '%s' incremented to %s", this.path, this.referenceCount);
|
||||
}
|
||||
}
|
||||
|
||||
void close() throws IOException {
|
||||
synchronized (this.lock) {
|
||||
if (this.referenceCount == 0) {
|
||||
return;
|
||||
}
|
||||
this.referenceCount--;
|
||||
if (this.referenceCount == 0) {
|
||||
debug.log("Closing '%s'", this.path);
|
||||
this.buffer = null;
|
||||
this.bufferPosition = -1;
|
||||
this.bufferSize = 0;
|
||||
this.fileChannel.close();
|
||||
if (tracker != null) {
|
||||
tracker.closedFileChannel(this.path, this.fileChannel);
|
||||
}
|
||||
this.fileChannel = null;
|
||||
}
|
||||
debug.log("Reference count for '%s' decremented to %s", this.path, this.referenceCount);
|
||||
}
|
||||
}
|
||||
|
||||
<E extends Exception> void ensureOpen(Supplier<E> exceptionSupplier) throws E {
|
||||
synchronized (this.lock) {
|
||||
if (this.referenceCount == 0) {
|
||||
throw exceptionSupplier.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.path.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal tracker used to check open and closing of files in tests.
|
||||
*/
|
||||
interface Tracker {
|
||||
|
||||
void openedFileChannel(Path path, FileChannel fileChannel);
|
||||
|
||||
void closedFileChannel(Path path, FileChannel fileChannel);
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.zip;
|
||||
|
||||
import java.util.BitSet;
|
||||
|
||||
/**
|
||||
* Tracks entries that have a name that should be offset by a specific amount. This class
|
||||
* is used with nested directory zip files so that entries under the directory are offset
|
||||
* correctly. META-INF entries are copied directly and have no offset.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class NameOffsetLookups {
|
||||
|
||||
public static final NameOffsetLookups NONE = new NameOffsetLookups(0, 0);
|
||||
|
||||
private final int offset;
|
||||
|
||||
private final BitSet enabled;
|
||||
|
||||
NameOffsetLookups(int offset, int size) {
|
||||
this.offset = offset;
|
||||
this.enabled = (size != 0) ? new BitSet(size) : null;
|
||||
}
|
||||
|
||||
void swap(int i, int j) {
|
||||
if (this.enabled != null) {
|
||||
boolean temp = this.enabled.get(i);
|
||||
this.enabled.set(i, this.enabled.get(j));
|
||||
this.enabled.set(j, temp);
|
||||
}
|
||||
}
|
||||
|
||||
int get(int index) {
|
||||
return isEnabled(index) ? this.offset : 0;
|
||||
}
|
||||
|
||||
int enable(int index, boolean enable) {
|
||||
if (this.enabled != null) {
|
||||
this.enabled.set(index, enable);
|
||||
}
|
||||
return (!enable) ? 0 : this.offset;
|
||||
}
|
||||
|
||||
boolean isEnabled(int index) {
|
||||
return (this.enabled != null && this.enabled.get(index));
|
||||
}
|
||||
|
||||
boolean hasAnyEnabled() {
|
||||
return this.enabled != null && this.enabled.cardinality() > 0;
|
||||
}
|
||||
|
||||
NameOffsetLookups emptyCopy() {
|
||||
return new NameOffsetLookups(this.offset, this.enabled.size());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.zip;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A virtual {@link DataBlock} build from a collection of other {@link DataBlock}
|
||||
* instances.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class VirtualDataBlock implements DataBlock {
|
||||
|
||||
private List<DataBlock> parts;
|
||||
|
||||
private long size;
|
||||
|
||||
/**
|
||||
* Create a new {@link VirtualDataBlock} instance. The {@link #setParts(Collection)}
|
||||
* method must be called before the data block can be used.
|
||||
*/
|
||||
protected VirtualDataBlock() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link VirtualDataBlock} backed by the given parts.
|
||||
* @param parts the parts that make up the virtual data block
|
||||
* @throws IOException in I/O error
|
||||
*/
|
||||
VirtualDataBlock(Collection<? extends DataBlock> parts) throws IOException {
|
||||
setParts(parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the parts that make up the virtual data block.
|
||||
* @param parts the data block parts
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
protected void setParts(Collection<? extends DataBlock> parts) throws IOException {
|
||||
this.parts = List.copyOf(parts);
|
||||
long size = 0;
|
||||
for (DataBlock part : parts) {
|
||||
size += part.size();
|
||||
}
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws IOException {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer dst, long pos) throws IOException {
|
||||
if (pos < 0 || pos >= this.size) {
|
||||
return -1;
|
||||
}
|
||||
long offset = 0;
|
||||
int result = 0;
|
||||
for (DataBlock part : this.parts) {
|
||||
while (pos >= offset && pos < offset + part.size()) {
|
||||
int count = part.read(dst, pos - offset);
|
||||
result += Math.max(count, 0);
|
||||
if (count <= 0 || !dst.hasRemaining()) {
|
||||
return result;
|
||||
}
|
||||
pos += count;
|
||||
}
|
||||
offset += part.size();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.zip;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link DataBlock} that creates a virtual zip. This class allows us to create virtual
|
||||
* zip files that can be parsed by regular JDK classes such as the zip {@link FileSystem}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class VirtualZipDataBlock extends VirtualDataBlock implements CloseableDataBlock {
|
||||
|
||||
private final FileChannelDataBlock data;
|
||||
|
||||
/**
|
||||
* Create a new {@link VirtualZipDataBlock} for the given entries.
|
||||
* @param data the source zip data
|
||||
* @param nameOffsetLookups the name offsets to apply
|
||||
* @param centralRecords the records that should be copied to the virtual zip
|
||||
* @param centralRecordPositions the record positions in the data block.
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
VirtualZipDataBlock(FileChannelDataBlock data, NameOffsetLookups nameOffsetLookups,
|
||||
ZipCentralDirectoryFileHeaderRecord[] centralRecords, long[] centralRecordPositions) throws IOException {
|
||||
this.data = data;
|
||||
List<DataBlock> parts = new ArrayList<>();
|
||||
List<DataBlock> centralParts = new ArrayList<>();
|
||||
long offset = 0;
|
||||
long sizeOfCentralDirectory = 0;
|
||||
for (int i = 0; i < centralRecords.length; i++) {
|
||||
ZipCentralDirectoryFileHeaderRecord centralRecord = centralRecords[i];
|
||||
int nameOffset = nameOffsetLookups.get(i);
|
||||
long centralRecordPos = centralRecordPositions[i];
|
||||
DataBlock name = new DataPart(
|
||||
centralRecordPos + ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET + nameOffset,
|
||||
(centralRecord.fileNameLength() & 0xFFFF) - nameOffset);
|
||||
ZipLocalFileHeaderRecord localRecord = ZipLocalFileHeaderRecord.load(this.data,
|
||||
centralRecord.offsetToLocalHeader());
|
||||
DataBlock content = new DataPart(centralRecord.offsetToLocalHeader() + localRecord.size(),
|
||||
centralRecord.compressedSize());
|
||||
sizeOfCentralDirectory += addToCentral(centralParts, centralRecord, centralRecordPos, name, (int) offset);
|
||||
offset += addToLocal(parts, localRecord, name, content);
|
||||
}
|
||||
parts.addAll(centralParts);
|
||||
ZipEndOfCentralDirectoryRecord eocd = new ZipEndOfCentralDirectoryRecord((short) centralRecords.length,
|
||||
(int) sizeOfCentralDirectory, (int) offset);
|
||||
parts.add(new ByteArrayDataBlock(eocd.asByteArray()));
|
||||
setParts(parts);
|
||||
}
|
||||
|
||||
private long addToCentral(List<DataBlock> parts, ZipCentralDirectoryFileHeaderRecord originalRecord,
|
||||
long originalRecordPos, DataBlock name, int offsetToLocalHeader) throws IOException {
|
||||
ZipCentralDirectoryFileHeaderRecord record = originalRecord.withFileNameLength((short) (name.size() & 0xFFFF))
|
||||
.withOffsetToLocalHeader(offsetToLocalHeader);
|
||||
int originalExtraFieldLength = originalRecord.extraFieldLength() & 0xFFFF;
|
||||
int originalFileCommentLength = originalRecord.fileCommentLength() & 0xFFFF;
|
||||
DataBlock extraFieldAndComment = new DataPart(
|
||||
originalRecordPos + originalRecord.size() - originalExtraFieldLength - originalFileCommentLength,
|
||||
originalExtraFieldLength + originalFileCommentLength);
|
||||
parts.add(new ByteArrayDataBlock(record.asByteArray()));
|
||||
parts.add(name);
|
||||
parts.add(extraFieldAndComment);
|
||||
return record.size();
|
||||
}
|
||||
|
||||
private long addToLocal(List<DataBlock> parts, ZipLocalFileHeaderRecord originalRecord, DataBlock name,
|
||||
DataBlock content) throws IOException {
|
||||
ZipLocalFileHeaderRecord record = originalRecord.withExtraFieldLength((short) 0)
|
||||
.withFileNameLength((short) (name.size() & 0xFFFF));
|
||||
parts.add(new ByteArrayDataBlock(record.asByteArray()));
|
||||
parts.add(name);
|
||||
parts.add(content);
|
||||
return record.size() + content.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
this.data.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link DataBlock} that points to part of the original data block.
|
||||
*/
|
||||
final class DataPart implements DataBlock {
|
||||
|
||||
private final long offset;
|
||||
|
||||
private final long size;
|
||||
|
||||
DataPart(long offset, long size) {
|
||||
this.offset = offset;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws IOException {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer dst, long pos) throws IOException {
|
||||
int remaining = (int) (this.size - pos);
|
||||
if (remaining <= 0) {
|
||||
return -1;
|
||||
}
|
||||
int originalLimit = -1;
|
||||
if (dst.remaining() > remaining) {
|
||||
originalLimit = dst.limit();
|
||||
dst.limit(dst.position() + remaining);
|
||||
}
|
||||
int result = VirtualZipDataBlock.this.data.read(dst, this.offset + pos);
|
||||
if (originalLimit != -1) {
|
||||
dst.limit(originalLimit);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.zip;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import org.springframework.boot.loader.log.DebugLogger;
|
||||
|
||||
/**
|
||||
* A Zip64 end of central directory locator.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @param pos the position where this record begins in the source {@link DataBlock}
|
||||
* @param numberOfThisDisk the number of the disk with the start of the zip64 end of
|
||||
* central directory
|
||||
* @param offsetToZip64EndOfCentralDirectoryRecord the relative offset of the zip64 end of
|
||||
* central directory record
|
||||
* @param totalNumberOfDisks the total number of disks
|
||||
* @see <a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT">Chapter
|
||||
* 4.3.15 of the Zip File Format Specification</a>
|
||||
*/
|
||||
record Zip64EndOfCentralDirectoryLocator(long pos, int numberOfThisDisk, long offsetToZip64EndOfCentralDirectoryRecord,
|
||||
int totalNumberOfDisks) {
|
||||
|
||||
private static final DebugLogger debug = DebugLogger.get(Zip64EndOfCentralDirectoryLocator.class);
|
||||
|
||||
private static final int SIGNATURE = 0x07064b50;
|
||||
|
||||
/**
|
||||
* The size of this record.
|
||||
*/
|
||||
static final int SIZE = 20;
|
||||
|
||||
/**
|
||||
* Return the {@link Zip64EndOfCentralDirectoryLocator} or {@code null} if this is not
|
||||
* a Zip64 file.
|
||||
* @param dataBlock the source data block
|
||||
* @param endOfCentralDirectoryPos the {@link ZipEndOfCentralDirectoryRecord} position
|
||||
* @return a {@link Zip64EndOfCentralDirectoryLocator} instance or null
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
static Zip64EndOfCentralDirectoryLocator find(DataBlock dataBlock, long endOfCentralDirectoryPos)
|
||||
throws IOException {
|
||||
debug.log("Finding Zip64EndOfCentralDirectoryLocator from EOCD at %s", endOfCentralDirectoryPos);
|
||||
long pos = endOfCentralDirectoryPos - SIZE;
|
||||
if (pos < 0) {
|
||||
debug.log("No Zip64EndOfCentralDirectoryLocator due to negative position %s", pos);
|
||||
return null;
|
||||
}
|
||||
ByteBuffer buffer = ByteBuffer.allocate(SIZE);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
dataBlock.read(buffer, pos);
|
||||
buffer.rewind();
|
||||
int signature = buffer.getInt();
|
||||
if (signature != SIGNATURE) {
|
||||
debug.log("Found incorrect Zip64EndOfCentralDirectoryLocator signature %s at position %s", signature, pos);
|
||||
return null;
|
||||
}
|
||||
debug.log("Found Zip64EndOfCentralDirectoryLocator at position %s", pos);
|
||||
return new Zip64EndOfCentralDirectoryLocator(pos, buffer.getInt(), buffer.getLong(), buffer.getInt());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.zip;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import org.springframework.boot.loader.log.DebugLogger;
|
||||
|
||||
/**
|
||||
* A Zip64 end of central directory record.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @param size the size of this record
|
||||
* @param sizeOfZip64EndOfCentralDirectoryRecord the size of zip64 end of central
|
||||
* directory record
|
||||
* @param versionMadeBy the version that made the zip
|
||||
* @param versionNeededToExtract the version needed to extract the zip
|
||||
* @param numberOfThisDisk the number of this disk
|
||||
* @param diskWhereCentralDirectoryStarts the disk where central directory starts
|
||||
* @param numberOfCentralDirectoryEntriesOnThisDisk the number of central directory
|
||||
* entries on this disk
|
||||
* @param totalNumberOfCentralDirectoryEntries the total number of central directory
|
||||
* entries
|
||||
* @param sizeOfCentralDirectory the size of central directory (bytes)
|
||||
* @param offsetToStartOfCentralDirectory the offset of start of central directory,
|
||||
* relative to start of archive
|
||||
* @see <a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT">Chapter
|
||||
* 4.3.14 of the Zip File Format Specification</a>
|
||||
*/
|
||||
record Zip64EndOfCentralDirectoryRecord(long size, long sizeOfZip64EndOfCentralDirectoryRecord, short versionMadeBy,
|
||||
short versionNeededToExtract, int numberOfThisDisk, int diskWhereCentralDirectoryStarts,
|
||||
long numberOfCentralDirectoryEntriesOnThisDisk, long totalNumberOfCentralDirectoryEntries,
|
||||
long sizeOfCentralDirectory, long offsetToStartOfCentralDirectory) {
|
||||
|
||||
private static final DebugLogger debug = DebugLogger.get(Zip64EndOfCentralDirectoryRecord.class);
|
||||
|
||||
private static final int SIGNATURE = 0x06064b50;
|
||||
|
||||
private static final int MINIMUM_SIZE = 56;
|
||||
|
||||
/**
|
||||
* Load the {@link Zip64EndOfCentralDirectoryRecord} from the given data block based
|
||||
* on the offset given in the locator.
|
||||
* @param dataBlock the source data block
|
||||
* @param locator the {@link Zip64EndOfCentralDirectoryLocator} or {@code null}
|
||||
* @return a new {@link ZipCentralDirectoryFileHeaderRecord} instance or {@code null}
|
||||
* if the locator is {@code null}
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
static Zip64EndOfCentralDirectoryRecord load(DataBlock dataBlock, Zip64EndOfCentralDirectoryLocator locator)
|
||||
throws IOException {
|
||||
if (locator == null) {
|
||||
return null;
|
||||
}
|
||||
ByteBuffer buffer = ByteBuffer.allocate(MINIMUM_SIZE);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
long size = locator.pos() - locator.offsetToZip64EndOfCentralDirectoryRecord();
|
||||
long pos = locator.pos() - size;
|
||||
debug.log("Loading Zip64EndOfCentralDirectoryRecord from position %s size %s", pos, size);
|
||||
dataBlock.readFully(buffer, pos);
|
||||
buffer.rewind();
|
||||
int signature = buffer.getInt();
|
||||
if (signature != SIGNATURE) {
|
||||
debug.log("Found incorrect Zip64EndOfCentralDirectoryRecord signature %s at position %s", signature, pos);
|
||||
throw new IOException("Zip64 'End Of Central Directory Record' not found at position " + pos
|
||||
+ ". Zip file is corrupt or includes prefixed bytes which are not supported with Zip64 files");
|
||||
}
|
||||
return new Zip64EndOfCentralDirectoryRecord(size, buffer.getLong(), buffer.getShort(), buffer.getShort(),
|
||||
buffer.getInt(), buffer.getInt(), buffer.getLong(), buffer.getLong(), buffer.getLong(),
|
||||
buffer.getLong());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.zip;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.temporal.ValueRange;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.springframework.boot.loader.log.DebugLogger;
|
||||
|
||||
/**
|
||||
* A ZIP File "Central directory file header record" (CDFH).
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @param versionMadeBy the version that made the zip
|
||||
* @param versionNeededToExtract the version needed to extract the zip
|
||||
* @param generalPurposeBitFlag the general purpose bit flag
|
||||
* @param compressionMethod the compression method used for this entry
|
||||
* @param lastModFileTime the last modified file time
|
||||
* @param lastModFileDate the last modified file date
|
||||
* @param crc32 the CRC32 checksum
|
||||
* @param compressedSize the size of the entry when compressed
|
||||
* @param uncompressedSize the size of the entry when uncompressed
|
||||
* @param fileNameLength the file name length
|
||||
* @param extraFieldLength the extra field length
|
||||
* @param fileCommentLength the comment length
|
||||
* @param diskNumberStart the disk number where the entry starts
|
||||
* @param internalFileAttributes the internal file attributes
|
||||
* @param externalFileAttributes the external file attributes
|
||||
* @param offsetToLocalHeader the relative offset to the local file header
|
||||
* @see <a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT">Chapter
|
||||
* 4.3.12 of the Zip File Format Specification</a>
|
||||
*/
|
||||
record ZipCentralDirectoryFileHeaderRecord(short versionMadeBy, short versionNeededToExtract,
|
||||
short generalPurposeBitFlag, short compressionMethod, short lastModFileTime, short lastModFileDate, int crc32,
|
||||
int compressedSize, int uncompressedSize, short fileNameLength, short extraFieldLength, short fileCommentLength,
|
||||
short diskNumberStart, short internalFileAttributes, int externalFileAttributes, int offsetToLocalHeader) {
|
||||
|
||||
private static final DebugLogger debug = DebugLogger.get(ZipCentralDirectoryFileHeaderRecord.class);
|
||||
|
||||
private static final int SIGNATURE = 0x02014b50;
|
||||
|
||||
private static final int MINIMUM_SIZE = 46;
|
||||
|
||||
/**
|
||||
* The offset of the file name relative to the record start position.
|
||||
*/
|
||||
static final int FILE_NAME_OFFSET = MINIMUM_SIZE;
|
||||
|
||||
/**
|
||||
* Return the size of this record.
|
||||
* @return the record size
|
||||
*/
|
||||
long size() {
|
||||
return MINIMUM_SIZE + fileNameLength() + extraFieldLength() + fileCommentLength();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy values from this block to the given {@link ZipEntry}.
|
||||
* @param dataBlock the source data block
|
||||
* @param pos the position of this {@link ZipCentralDirectoryFileHeaderRecord}
|
||||
* @param zipEntry the destination zip entry
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
void copyTo(DataBlock dataBlock, long pos, ZipEntry zipEntry) throws IOException {
|
||||
int fileNameLength = fileNameLength() & 0xFFFF;
|
||||
int extraLength = extraFieldLength() & 0xFFFF;
|
||||
int commentLength = fileCommentLength() & 0xFFFF;
|
||||
zipEntry.setMethod(compressionMethod() & 0xFFFF);
|
||||
zipEntry.setTime(decodeMsDosFormatDateTime(lastModFileDate(), lastModFileTime()));
|
||||
zipEntry.setCrc(crc32() & 0xFFFFFFFFL);
|
||||
zipEntry.setCompressedSize(compressedSize() & 0xFFFFFFFFL);
|
||||
zipEntry.setSize(uncompressedSize() & 0xFFFFFFFFL);
|
||||
if (extraLength > 0) {
|
||||
long extraPos = pos + MINIMUM_SIZE + fileNameLength;
|
||||
ByteBuffer buffer = ByteBuffer.allocate(extraLength);
|
||||
dataBlock.readFully(buffer, extraPos);
|
||||
zipEntry.setExtra(buffer.array());
|
||||
}
|
||||
if ((fileCommentLength() & 0xFFFF) > 0) {
|
||||
long commentPos = MINIMUM_SIZE + fileNameLength + extraLength;
|
||||
zipEntry.setComment(ZipString.readString(dataBlock, commentPos, commentLength));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 date the date
|
||||
* @param time the time
|
||||
* @return the date and time as milliseconds since the epoch
|
||||
*/
|
||||
private long decodeMsDosFormatDateTime(short date, short time) {
|
||||
int year = getChronoValue(((date >> 9) & 0x7f) + 1980, ChronoField.YEAR);
|
||||
int month = getChronoValue((date >> 5) & 0x0f, ChronoField.MONTH_OF_YEAR);
|
||||
int day = getChronoValue(date & 0x1f, ChronoField.DAY_OF_MONTH);
|
||||
int hour = getChronoValue((time >> 11) & 0x1f, ChronoField.HOUR_OF_DAY);
|
||||
int minute = getChronoValue((time >> 5) & 0x3f, ChronoField.MINUTE_OF_HOUR);
|
||||
int second = getChronoValue((time << 1) & 0x3e, ChronoField.SECOND_OF_MINUTE);
|
||||
return ZonedDateTime.of(year, month, day, hour, minute, second, 0, ZoneId.systemDefault())
|
||||
.toInstant()
|
||||
.truncatedTo(ChronoUnit.SECONDS)
|
||||
.toEpochMilli();
|
||||
}
|
||||
|
||||
private static int getChronoValue(long value, ChronoField field) {
|
||||
ValueRange range = field.range();
|
||||
return Math.toIntExact(Math.min(Math.max(value, range.getMinimum()), range.getMaximum()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ZipCentralDirectoryFileHeaderRecord} with a new
|
||||
* {@link #fileNameLength()}.
|
||||
* @param fileNameLength the new file name length
|
||||
* @return a new {@link ZipCentralDirectoryFileHeaderRecord} instance
|
||||
*/
|
||||
ZipCentralDirectoryFileHeaderRecord withFileNameLength(short fileNameLength) {
|
||||
return (this.fileNameLength != fileNameLength) ? new ZipCentralDirectoryFileHeaderRecord(this.versionMadeBy,
|
||||
this.versionNeededToExtract, this.generalPurposeBitFlag, this.compressionMethod, this.lastModFileTime,
|
||||
this.lastModFileDate, this.crc32, this.compressedSize, this.uncompressedSize, fileNameLength,
|
||||
this.extraFieldLength, this.fileCommentLength, this.diskNumberStart, this.internalFileAttributes,
|
||||
this.externalFileAttributes, this.offsetToLocalHeader) : this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ZipCentralDirectoryFileHeaderRecord} with a new
|
||||
* {@link #offsetToLocalHeader()}.
|
||||
* @param offsetToLocalHeader the new offset to local header
|
||||
* @return a new {@link ZipCentralDirectoryFileHeaderRecord} instance
|
||||
*/
|
||||
ZipCentralDirectoryFileHeaderRecord withOffsetToLocalHeader(int offsetToLocalHeader) {
|
||||
return (this.offsetToLocalHeader != offsetToLocalHeader) ? new ZipCentralDirectoryFileHeaderRecord(
|
||||
this.versionMadeBy, this.versionNeededToExtract, this.generalPurposeBitFlag, this.compressionMethod,
|
||||
this.lastModFileTime, this.lastModFileDate, this.crc32, this.compressedSize, this.uncompressedSize,
|
||||
this.fileNameLength, this.extraFieldLength, this.fileCommentLength, this.diskNumberStart,
|
||||
this.internalFileAttributes, this.externalFileAttributes, offsetToLocalHeader) : this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the contents of this record as a byte array suitable for writing to a zip.
|
||||
* @return the record as a byte array
|
||||
*/
|
||||
byte[] asByteArray() {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(MINIMUM_SIZE);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer.putInt(SIGNATURE);
|
||||
buffer.putShort(this.versionMadeBy);
|
||||
buffer.putShort(this.versionNeededToExtract);
|
||||
buffer.putShort(this.generalPurposeBitFlag);
|
||||
buffer.putShort(this.compressionMethod);
|
||||
buffer.putShort(this.lastModFileTime);
|
||||
buffer.putShort(this.lastModFileDate);
|
||||
buffer.putInt(this.crc32);
|
||||
buffer.putInt(this.compressedSize);
|
||||
buffer.putInt(this.uncompressedSize);
|
||||
buffer.putShort(this.fileNameLength);
|
||||
buffer.putShort(this.extraFieldLength);
|
||||
buffer.putShort(this.fileCommentLength);
|
||||
buffer.putShort(this.diskNumberStart);
|
||||
buffer.putShort(this.internalFileAttributes);
|
||||
buffer.putInt(this.externalFileAttributes);
|
||||
buffer.putInt(this.offsetToLocalHeader);
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the {@link ZipCentralDirectoryFileHeaderRecord} from the given data block.
|
||||
* @param dataBlock the source data block
|
||||
* @param pos the position of the record
|
||||
* @return a new {@link ZipCentralDirectoryFileHeaderRecord} instance
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
static ZipCentralDirectoryFileHeaderRecord load(DataBlock dataBlock, long pos) throws IOException {
|
||||
debug.log("Loading CentralDirectoryFileHeaderRecord from position %s", pos);
|
||||
ByteBuffer buffer = ByteBuffer.allocate(MINIMUM_SIZE);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
dataBlock.readFully(buffer, pos);
|
||||
buffer.rewind();
|
||||
int signature = buffer.getInt();
|
||||
if (signature != SIGNATURE) {
|
||||
debug.log("Found incorrect CentralDirectoryFileHeaderRecord signature %s at position %s", signature, pos);
|
||||
throw new IOException("Zip 'Central Directory File Header Record' not found at position " + pos);
|
||||
}
|
||||
return new ZipCentralDirectoryFileHeaderRecord(buffer.getShort(), buffer.getShort(), buffer.getShort(),
|
||||
buffer.getShort(), buffer.getShort(), buffer.getShort(), buffer.getInt(), buffer.getInt(),
|
||||
buffer.getInt(), buffer.getShort(), buffer.getShort(), buffer.getShort(), buffer.getShort(),
|
||||
buffer.getShort(), buffer.getInt(), buffer.getInt());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,811 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.zip;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.ref.Cleaner.Cleanable;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.springframework.boot.loader.log.DebugLogger;
|
||||
|
||||
/**
|
||||
* Provides raw access to content from a regular or nested zip file. This class performs
|
||||
* the low level parsing of a zip file and provide access to raw entry data that it
|
||||
* contains. Unlike {@link java.util.zip.ZipFile}, this implementation can load content
|
||||
* from a zip file nested inside another file as long as the entry is not compressed.
|
||||
* <p>
|
||||
* In order to reduce memory consumption, this implementation stores only the the hash of
|
||||
* the entry names, the central directory offsets and the original positions. Entries are
|
||||
* stored internally in {@code hashCode} order so that a binary search can be used to
|
||||
* quickly find an entry by name or determine if the zip file doesn't have a given entry.
|
||||
* <p>
|
||||
* {@link ZipContent} for a typical Spring Boot application JAR will have somewhere in the
|
||||
* region of 10,500 entries which should consume about 122K.
|
||||
* <p>
|
||||
* {@link ZipContent} results are cached and it is assumed that zip content will not
|
||||
* change once loaded. Entries and Strings are not cached and will be recreated on each
|
||||
* access which may produce a lot of garbage.
|
||||
* <p>
|
||||
* This implementation does not use {@link Cleanable} so care must be taken to release
|
||||
* {@link ZipContent} resources. The {@link #close()} method should be called explicitly
|
||||
* or by try-with-resources. Care must be take to only call close once.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public final class ZipContent implements Closeable {
|
||||
|
||||
private static final String META_INF = "META-INF/";
|
||||
|
||||
private static final byte[] SIGNATURE_SUFFIX = ".DSA".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
private static final DebugLogger debug = DebugLogger.get(ZipContent.class);
|
||||
|
||||
private static final Map<Source, ZipContent> cache = new ConcurrentHashMap<>();
|
||||
|
||||
private final Source source;
|
||||
|
||||
private final FileChannelDataBlock data;
|
||||
|
||||
private final long centralDirectoryPos;
|
||||
|
||||
private final long commentPos;
|
||||
|
||||
private final long commentLength;
|
||||
|
||||
private final int[] lookupIndexes;
|
||||
|
||||
private final int[] nameHashLookups;
|
||||
|
||||
private final int[] relativeCentralDirectoryOffsetLookups;
|
||||
|
||||
private final NameOffsetLookups nameOffsetLookups;
|
||||
|
||||
private final boolean hasJarSignatureFile;
|
||||
|
||||
private SoftReference<CloseableDataBlock> virtualData;
|
||||
|
||||
private SoftReference<Map<Class<?>, Object>> info;
|
||||
|
||||
private ZipContent(Source source, FileChannelDataBlock data, long centralDirectoryPos, long commentPos,
|
||||
long commentLength, int[] lookupIndexes, int[] nameHashLookups, int[] relativeCentralDirectoryOffsetLookups,
|
||||
NameOffsetLookups nameOffsetLookups, boolean hasJarSignatureFile) {
|
||||
this.source = source;
|
||||
this.data = data;
|
||||
this.centralDirectoryPos = centralDirectoryPos;
|
||||
this.commentPos = commentPos;
|
||||
this.commentLength = commentLength;
|
||||
this.lookupIndexes = lookupIndexes;
|
||||
this.nameHashLookups = nameHashLookups;
|
||||
this.relativeCentralDirectoryOffsetLookups = relativeCentralDirectoryOffsetLookups;
|
||||
this.nameOffsetLookups = nameOffsetLookups;
|
||||
this.hasJarSignatureFile = hasJarSignatureFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a {@link DataBlock} containing the raw zip data. For container zip files, this
|
||||
* may be smaller than the original file since additional bytes are permitted at the
|
||||
* front of a zip file. For nested zip files, this will be only the contents of the
|
||||
* nest zip.
|
||||
* <p>
|
||||
* For nested directory zip files, a virtual data block will be created containing
|
||||
* only the relevant content.
|
||||
* <p>
|
||||
* To release resources, the {@link #close()} method of the data block should be
|
||||
* called explicitly or by try-with-resources.
|
||||
* <p>
|
||||
* The returned data block should not be accessed once {@link #close()} has been
|
||||
* called.
|
||||
* @return the zip data
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
public CloseableDataBlock openRawZipData() throws IOException {
|
||||
this.data.open();
|
||||
return (!this.nameOffsetLookups.hasAnyEnabled()) ? this.data : getVirtualData();
|
||||
}
|
||||
|
||||
private CloseableDataBlock getVirtualData() throws IOException {
|
||||
CloseableDataBlock virtualData = (this.virtualData != null) ? this.virtualData.get() : null;
|
||||
if (virtualData != null) {
|
||||
return virtualData;
|
||||
}
|
||||
virtualData = createVirtualData();
|
||||
this.virtualData = new SoftReference<>(virtualData);
|
||||
return virtualData;
|
||||
}
|
||||
|
||||
private CloseableDataBlock createVirtualData() throws IOException {
|
||||
int size = size();
|
||||
NameOffsetLookups nameOffsetLookups = this.nameOffsetLookups.emptyCopy();
|
||||
ZipCentralDirectoryFileHeaderRecord[] centralRecords = new ZipCentralDirectoryFileHeaderRecord[size];
|
||||
long[] centralRecordPositions = new long[size];
|
||||
for (int i = 0; i < size; i++) {
|
||||
int lookupIndex = ZipContent.this.lookupIndexes[i];
|
||||
long pos = getCentralDirectoryFileHeaderRecordPos(lookupIndex);
|
||||
nameOffsetLookups.enable(i, this.nameOffsetLookups.isEnabled(lookupIndex));
|
||||
centralRecords[i] = ZipCentralDirectoryFileHeaderRecord.load(this.data, pos);
|
||||
centralRecordPositions[i] = pos;
|
||||
}
|
||||
return new VirtualZipDataBlock(this.data, nameOffsetLookups, centralRecords, centralRecordPositions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of entries in the ZIP file.
|
||||
* @return the number of entries
|
||||
*/
|
||||
public int size() {
|
||||
return this.lookupIndexes.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the zip comment, if any.
|
||||
* @return the comment or {@code null}
|
||||
*/
|
||||
public String getComment() {
|
||||
try {
|
||||
return ZipString.readString(this.data, this.commentPos, this.commentLength);
|
||||
}
|
||||
catch (UncheckedIOException ex) {
|
||||
if (ex.getCause() instanceof ClosedChannelException) {
|
||||
throw new IllegalStateException("Zip content closed", ex);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the entry with the given name, if any.
|
||||
* @param name the name of the entry to find
|
||||
* @return the entry or {@code null}
|
||||
*/
|
||||
public Entry getEntry(CharSequence name) {
|
||||
return getEntry(null, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the entry with the given name, if any.
|
||||
* @param namePrefix an optional prefix for the name
|
||||
* @param name the name of the entry to find
|
||||
* @return the entry or {@code null}
|
||||
*/
|
||||
public Entry getEntry(CharSequence namePrefix, CharSequence name) {
|
||||
int nameHash = nameHash(namePrefix, name);
|
||||
int lookupIndex = getFirstLookupIndex(nameHash);
|
||||
int size = size();
|
||||
while (lookupIndex >= 0 && lookupIndex < size && this.nameHashLookups[lookupIndex] == nameHash) {
|
||||
long pos = getCentralDirectoryFileHeaderRecordPos(lookupIndex);
|
||||
ZipCentralDirectoryFileHeaderRecord centralRecord = loadZipCentralDirectoryFileHeaderRecord(pos);
|
||||
if (hasName(lookupIndex, centralRecord, pos, namePrefix, name)) {
|
||||
return new Entry(lookupIndex, centralRecord);
|
||||
}
|
||||
lookupIndex++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if an entry with the given name exists.
|
||||
* @param namePrefix an optional prefix for the name
|
||||
* @param name the name of the entry to find
|
||||
* @return the entry or {@code null}
|
||||
*/
|
||||
public boolean hasEntry(CharSequence namePrefix, CharSequence name) {
|
||||
int nameHash = nameHash(namePrefix, name);
|
||||
int lookupIndex = getFirstLookupIndex(nameHash);
|
||||
int size = size();
|
||||
while (lookupIndex >= 0 && lookupIndex < size && this.nameHashLookups[lookupIndex] == nameHash) {
|
||||
long pos = getCentralDirectoryFileHeaderRecordPos(lookupIndex);
|
||||
ZipCentralDirectoryFileHeaderRecord centralRecord = loadZipCentralDirectoryFileHeaderRecord(pos);
|
||||
if (hasName(lookupIndex, centralRecord, pos, namePrefix, name)) {
|
||||
return true;
|
||||
}
|
||||
lookupIndex++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the entry at the specified index.
|
||||
* @param index the entry index
|
||||
* @return the entry
|
||||
* @throws IndexOutOfBoundsException if the index is out of bounds
|
||||
*/
|
||||
public Entry getEntry(int index) {
|
||||
int lookupIndex = ZipContent.this.lookupIndexes[index];
|
||||
long pos = getCentralDirectoryFileHeaderRecordPos(lookupIndex);
|
||||
ZipCentralDirectoryFileHeaderRecord centralRecord = loadZipCentralDirectoryFileHeaderRecord(pos);
|
||||
return new Entry(lookupIndex, centralRecord);
|
||||
}
|
||||
|
||||
private ZipCentralDirectoryFileHeaderRecord loadZipCentralDirectoryFileHeaderRecord(long pos) {
|
||||
try {
|
||||
return ZipCentralDirectoryFileHeaderRecord.load(this.data, pos);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
if (ex instanceof ClosedChannelException) {
|
||||
throw new IllegalStateException("Zip content closed", ex);
|
||||
}
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private int nameHash(CharSequence namePrefix, CharSequence name) {
|
||||
int nameHash = 0;
|
||||
nameHash = (namePrefix != null) ? ZipString.hash(nameHash, namePrefix, false) : nameHash;
|
||||
nameHash = ZipString.hash(nameHash, name, true);
|
||||
return nameHash;
|
||||
}
|
||||
|
||||
private int getFirstLookupIndex(int nameHash) {
|
||||
int lookupIndex = Arrays.binarySearch(this.nameHashLookups, 0, this.nameHashLookups.length, nameHash);
|
||||
if (lookupIndex < 0) {
|
||||
return -1;
|
||||
}
|
||||
while (lookupIndex > 0 && this.nameHashLookups[lookupIndex - 1] == nameHash) {
|
||||
lookupIndex--;
|
||||
}
|
||||
return lookupIndex;
|
||||
}
|
||||
|
||||
private long getCentralDirectoryFileHeaderRecordPos(int lookupIndex) {
|
||||
return this.centralDirectoryPos + this.relativeCentralDirectoryOffsetLookups[lookupIndex];
|
||||
}
|
||||
|
||||
private boolean hasName(int lookupIndex, ZipCentralDirectoryFileHeaderRecord centralRecord, long pos,
|
||||
CharSequence namePrefix, CharSequence name) {
|
||||
int offset = this.nameOffsetLookups.get(lookupIndex);
|
||||
pos += ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET + offset;
|
||||
int len = centralRecord.fileNameLength() - offset;
|
||||
ByteBuffer buffer = ByteBuffer.allocate(ZipString.BUFFER_SIZE);
|
||||
if (namePrefix != null) {
|
||||
int startsWithNamePrefix = ZipString.startsWith(buffer, this.data, pos, len, namePrefix);
|
||||
if (startsWithNamePrefix == -1) {
|
||||
return false;
|
||||
}
|
||||
pos += startsWithNamePrefix;
|
||||
len -= startsWithNamePrefix;
|
||||
}
|
||||
return ZipString.matches(buffer, this.data, pos, len, name, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or compute information based on the {@link ZipContent}.
|
||||
* @param <I> the info type to get or compute
|
||||
* @param type the info type to get or compute
|
||||
* @param function the function used to compute the information
|
||||
* @return the computed or existing information
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <I> I getInfo(Class<I> type, Function<ZipContent, I> function) {
|
||||
Map<Class<?>, Object> info = (this.info != null) ? this.info.get() : null;
|
||||
if (info == null) {
|
||||
info = new ConcurrentHashMap<>();
|
||||
this.info = new SoftReference<>(info);
|
||||
}
|
||||
return (I) info.computeIfAbsent(type, (key) -> {
|
||||
debug.log("Getting %s info from zip '%s'", type.getName(), this);
|
||||
return function.apply(this);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this zip contains a jar signature file
|
||||
* ({@code META-INF/*.DSA}).
|
||||
* @return if the zip contains a jar signature file
|
||||
*/
|
||||
public boolean hasJarSignatureFile() {
|
||||
return this.hasJarSignatureFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close this jar file, releasing the underlying file if this was the last reference.
|
||||
* @see java.io.Closeable#close()
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
this.data.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.source.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open {@link ZipContent} from the specified path. The resulting {@link ZipContent}
|
||||
* <em>must</em> be {@link #close() closed} by the caller.
|
||||
* @param path the zip path
|
||||
* @return a {@link ZipContent} instance
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
public static ZipContent open(Path path) throws IOException {
|
||||
return open(new Source(path.toAbsolutePath(), null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Open nested {@link ZipContent} from the specified path. The resulting
|
||||
* {@link ZipContent} <em>must</em> be {@link #close() closed} by the caller.
|
||||
* @param path the zip path
|
||||
* @param nestedEntryName the nested entry name to open
|
||||
* @return a {@link ZipContent} instance
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
public static ZipContent open(Path path, String nestedEntryName) throws IOException {
|
||||
return open(new Source(path.toAbsolutePath(), nestedEntryName));
|
||||
}
|
||||
|
||||
private static ZipContent open(Source source) throws IOException {
|
||||
ZipContent zipContent = cache.get(source);
|
||||
if (zipContent != null) {
|
||||
debug.log("Opening existing cached zip content for %s", zipContent);
|
||||
zipContent.data.open();
|
||||
return zipContent;
|
||||
}
|
||||
debug.log("Loading zip content from %s", source);
|
||||
zipContent = Loader.load(source);
|
||||
ZipContent previouslyCached = cache.putIfAbsent(source, zipContent);
|
||||
if (previouslyCached != null) {
|
||||
debug.log("Closing zip content from %s since cache was populated from another thread", source);
|
||||
zipContent.close();
|
||||
previouslyCached.data.open();
|
||||
return previouslyCached;
|
||||
}
|
||||
return zipContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* The source of {@link ZipContent}. Used as a cache key.
|
||||
*
|
||||
* @param path the path of the zip or container zip
|
||||
* @param nestedEntryName the name of the nested entry to use or {@code null}
|
||||
*/
|
||||
private record Source(Path path, String nestedEntryName) {
|
||||
|
||||
/**
|
||||
* Return if this is the source of a nested zip.
|
||||
* @return if this is for a nested zip
|
||||
*/
|
||||
boolean isNested() {
|
||||
return this.nestedEntryName != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return (!isNested()) ? path().toString() : path() + "[" + nestedEntryName() + "]";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal class used to load the zip content create a new {@link ZipContent}
|
||||
* instance.
|
||||
*/
|
||||
private static final class Loader {
|
||||
|
||||
private final ByteBuffer buffer = ByteBuffer.allocate(ZipString.BUFFER_SIZE);
|
||||
|
||||
private final Source source;
|
||||
|
||||
private final FileChannelDataBlock data;
|
||||
|
||||
private final long centralDirectoryPos;
|
||||
|
||||
private final int[] index;
|
||||
|
||||
private int[] nameHashLookups;
|
||||
|
||||
private int[] relativeCentralDirectoryOffsetLookups;
|
||||
|
||||
private final NameOffsetLookups nameOffsetLookups;
|
||||
|
||||
private int cursor;
|
||||
|
||||
private Loader(Source source, Entry directoryEntry, FileChannelDataBlock data, long centralDirectoryPos,
|
||||
int maxSize) {
|
||||
this.source = source;
|
||||
this.data = data;
|
||||
this.centralDirectoryPos = centralDirectoryPos;
|
||||
this.index = new int[maxSize];
|
||||
this.nameHashLookups = new int[maxSize];
|
||||
this.relativeCentralDirectoryOffsetLookups = new int[maxSize];
|
||||
this.nameOffsetLookups = (directoryEntry != null)
|
||||
? new NameOffsetLookups(directoryEntry.getName().length(), maxSize) : NameOffsetLookups.NONE;
|
||||
}
|
||||
|
||||
private void add(ZipCentralDirectoryFileHeaderRecord centralRecord, long pos, boolean enableNameOffset)
|
||||
throws IOException {
|
||||
int nameOffset = this.nameOffsetLookups.enable(this.cursor, enableNameOffset);
|
||||
int hash = ZipString.hash(this.buffer, this.data,
|
||||
pos + ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET + nameOffset,
|
||||
centralRecord.fileNameLength() - nameOffset, true);
|
||||
this.nameHashLookups[this.cursor] = hash;
|
||||
this.relativeCentralDirectoryOffsetLookups[this.cursor] = (int) ((pos - this.centralDirectoryPos));
|
||||
this.index[this.cursor] = this.cursor;
|
||||
this.cursor++;
|
||||
}
|
||||
|
||||
private ZipContent finish(long commentPos, long commentLength, boolean hasJarSignatureFile) {
|
||||
if (this.cursor != this.nameHashLookups.length) {
|
||||
this.nameHashLookups = Arrays.copyOf(this.nameHashLookups, this.cursor);
|
||||
this.relativeCentralDirectoryOffsetLookups = Arrays.copyOf(this.relativeCentralDirectoryOffsetLookups,
|
||||
this.cursor);
|
||||
}
|
||||
int size = this.nameHashLookups.length;
|
||||
sort(0, size - 1);
|
||||
int[] lookupIndexes = new int[size];
|
||||
for (int i = 0; i < size; i++) {
|
||||
lookupIndexes[this.index[i]] = i;
|
||||
}
|
||||
return new ZipContent(this.source, this.data, this.centralDirectoryPos, commentPos, commentLength,
|
||||
lookupIndexes, this.nameHashLookups, this.relativeCentralDirectoryOffsetLookups,
|
||||
this.nameOffsetLookups, hasJarSignatureFile);
|
||||
}
|
||||
|
||||
private void sort(int left, int right) {
|
||||
// Quick sort algorithm, uses nameHashCode as the source but sorts all arrays
|
||||
if (left < right) {
|
||||
int pivot = this.nameHashLookups[left + (right - left) / 2];
|
||||
int i = left;
|
||||
int j = right;
|
||||
while (i <= j) {
|
||||
while (this.nameHashLookups[i] < pivot) {
|
||||
i++;
|
||||
}
|
||||
while (this.nameHashLookups[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.index, i, j);
|
||||
swap(this.nameHashLookups, i, j);
|
||||
swap(this.relativeCentralDirectoryOffsetLookups, i, j);
|
||||
this.nameOffsetLookups.swap(i, j);
|
||||
}
|
||||
|
||||
private static void swap(int[] array, int i, int j) {
|
||||
int temp = array[i];
|
||||
array[i] = array[j];
|
||||
array[j] = temp;
|
||||
}
|
||||
|
||||
static ZipContent load(Source source) throws IOException {
|
||||
if (!source.isNested()) {
|
||||
return loadNonNested(source);
|
||||
}
|
||||
try (ZipContent zip = open(source.path())) {
|
||||
Entry entry = zip.getEntry(source.nestedEntryName());
|
||||
if (entry == null) {
|
||||
throw new IOException("Nested entry '%s' not found in container zip '%s'"
|
||||
.formatted(source.nestedEntryName(), source.path()));
|
||||
}
|
||||
return (!entry.isDirectory()) ? loadNestedZip(source, entry) : loadNestedDirectory(source, zip, entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static ZipContent loadNonNested(Source source) throws IOException {
|
||||
debug.log("Loading non-nested zip '%s'", source.path());
|
||||
return openAndLoad(source, new FileChannelDataBlock(source.path()));
|
||||
}
|
||||
|
||||
private static ZipContent loadNestedZip(Source source, Entry entry) throws IOException {
|
||||
if (entry.centralRecord.compressionMethod() != ZipEntry.STORED) {
|
||||
throw new IOException("Nested entry '%s' in container zip '%s' must not be compressed"
|
||||
.formatted(source.nestedEntryName(), source.path()));
|
||||
}
|
||||
debug.log("Loading nested zip entry '%s' from '%s'", source.nestedEntryName(), source.path());
|
||||
return openAndLoad(source, entry.getContent());
|
||||
}
|
||||
|
||||
private static ZipContent openAndLoad(Source source, FileChannelDataBlock data) throws IOException {
|
||||
try {
|
||||
data.open();
|
||||
return loadContent(source, data);
|
||||
}
|
||||
catch (IOException | RuntimeException ex) {
|
||||
data.close();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private static ZipContent loadContent(Source source, FileChannelDataBlock data) throws IOException {
|
||||
ZipEndOfCentralDirectoryRecord.Located locatedEocd = ZipEndOfCentralDirectoryRecord.load(data);
|
||||
ZipEndOfCentralDirectoryRecord eocd = locatedEocd.endOfCentralDirectoryRecord();
|
||||
long eocdPos = locatedEocd.pos();
|
||||
Zip64EndOfCentralDirectoryLocator zip64Locator = Zip64EndOfCentralDirectoryLocator.find(data, eocdPos);
|
||||
Zip64EndOfCentralDirectoryRecord zip64Eocd = Zip64EndOfCentralDirectoryRecord.load(data, zip64Locator);
|
||||
data = data.slice(getStartOfZipContent(data, eocd, zip64Eocd));
|
||||
long centralDirectoryPos = (zip64Eocd != null) ? zip64Eocd.offsetToStartOfCentralDirectory()
|
||||
: eocd.offsetToStartOfCentralDirectory();
|
||||
long numberOfEntries = (zip64Eocd != null) ? zip64Eocd.totalNumberOfCentralDirectoryEntries()
|
||||
: eocd.totalNumberOfCentralDirectoryEntries();
|
||||
if (numberOfEntries > 0xFFFFFFFFL) {
|
||||
throw new IllegalStateException("Too many zip entries in " + source);
|
||||
}
|
||||
Loader loader = new Loader(source, null, data, centralDirectoryPos, (int) (numberOfEntries & 0xFFFFFFFFL));
|
||||
ByteBuffer signatureNameSuffixBuffer = ByteBuffer.allocate(SIGNATURE_SUFFIX.length);
|
||||
boolean hasJarSignatureFile = false;
|
||||
long pos = centralDirectoryPos;
|
||||
for (int i = 0; i < numberOfEntries; i++) {
|
||||
ZipCentralDirectoryFileHeaderRecord centralRecord = ZipCentralDirectoryFileHeaderRecord.load(data, pos);
|
||||
if (!hasJarSignatureFile) {
|
||||
long filenamePos = pos + ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET;
|
||||
if (centralRecord.fileNameLength() > SIGNATURE_SUFFIX.length && ZipString.startsWith(loader.buffer,
|
||||
data, filenamePos, centralRecord.fileNameLength(), META_INF) >= 0) {
|
||||
signatureNameSuffixBuffer.clear();
|
||||
data.readFully(signatureNameSuffixBuffer,
|
||||
filenamePos + centralRecord.fileNameLength() - SIGNATURE_SUFFIX.length);
|
||||
hasJarSignatureFile = Arrays.equals(SIGNATURE_SUFFIX, signatureNameSuffixBuffer.array());
|
||||
}
|
||||
}
|
||||
loader.add(centralRecord, pos, false);
|
||||
pos += centralRecord.size();
|
||||
}
|
||||
long commentPos = locatedEocd.pos() + ZipEndOfCentralDirectoryRecord.COMMENT_OFFSET;
|
||||
return loader.finish(commentPos, eocd.commentLength(), hasJarSignatureFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param eocd the end of central directory record
|
||||
* @param zip64Eocd the zip64 end of central directory record or {@code null}
|
||||
* @return the offset within the data where the archive begins
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
private static long getStartOfZipContent(FileChannelDataBlock data, ZipEndOfCentralDirectoryRecord eocd,
|
||||
Zip64EndOfCentralDirectoryRecord zip64Eocd) throws IOException {
|
||||
long specifiedOffsetToStartOfCentralDirectory = (zip64Eocd != null)
|
||||
? zip64Eocd.offsetToStartOfCentralDirectory() : eocd.offsetToStartOfCentralDirectory();
|
||||
long sizeOfCentralDirectoryAndEndRecords = getSizeOfCentralDirectoryAndEndRecords(eocd, zip64Eocd);
|
||||
long actualOffsetToStartOfCentralDirectory = data.size() - sizeOfCentralDirectoryAndEndRecords;
|
||||
return actualOffsetToStartOfCentralDirectory - specifiedOffsetToStartOfCentralDirectory;
|
||||
}
|
||||
|
||||
private static long getSizeOfCentralDirectoryAndEndRecords(ZipEndOfCentralDirectoryRecord eocd,
|
||||
Zip64EndOfCentralDirectoryRecord zip64Eocd) {
|
||||
long result = 0;
|
||||
result += eocd.size();
|
||||
if (zip64Eocd != null) {
|
||||
result += Zip64EndOfCentralDirectoryLocator.SIZE;
|
||||
result += zip64Eocd.size();
|
||||
}
|
||||
result += (zip64Eocd != null) ? zip64Eocd.sizeOfCentralDirectory() : eocd.sizeOfCentralDirectory();
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ZipContent loadNestedDirectory(Source source, ZipContent zip, Entry directoryEntry)
|
||||
throws IOException {
|
||||
debug.log("Loading nested directry entry '%s' from '%s'", source.nestedEntryName(), source.path());
|
||||
if (!source.nestedEntryName().endsWith("/")) {
|
||||
throw new IllegalArgumentException("Nested entry name must end with '/'");
|
||||
}
|
||||
String directoryName = directoryEntry.getName();
|
||||
zip.data.open();
|
||||
try {
|
||||
Loader loader = new Loader(source, directoryEntry, zip.data, zip.centralDirectoryPos, zip.size());
|
||||
for (int cursor = 0; cursor < zip.size(); cursor++) {
|
||||
int index = zip.lookupIndexes[cursor];
|
||||
if (index != directoryEntry.getLookupIndex()) {
|
||||
long pos = zip.getCentralDirectoryFileHeaderRecordPos(index);
|
||||
ZipCentralDirectoryFileHeaderRecord centralRecord = ZipCentralDirectoryFileHeaderRecord
|
||||
.load(zip.data, pos);
|
||||
long namePos = pos + ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET;
|
||||
short nameLen = centralRecord.fileNameLength();
|
||||
if (ZipString.startsWith(loader.buffer, zip.data, namePos, nameLen, META_INF) != -1) {
|
||||
loader.add(centralRecord, pos, false);
|
||||
}
|
||||
else if (ZipString.startsWith(loader.buffer, zip.data, namePos, nameLen, directoryName) != -1) {
|
||||
loader.add(centralRecord, pos, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return loader.finish(zip.commentPos, zip.commentLength, zip.hasJarSignatureFile);
|
||||
}
|
||||
catch (IOException | RuntimeException ex) {
|
||||
zip.data.close();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A single zip content entry.
|
||||
*/
|
||||
public class Entry {
|
||||
|
||||
private final int lookupIndex;
|
||||
|
||||
private final ZipCentralDirectoryFileHeaderRecord centralRecord;
|
||||
|
||||
private volatile String name;
|
||||
|
||||
private volatile FileChannelDataBlock content;
|
||||
|
||||
/**
|
||||
* Create a new {@link Entry} instance.
|
||||
* @param lookupIndex the lookup index of the entry
|
||||
* @param centralRecord the {@link ZipCentralDirectoryFileHeaderRecord} for the
|
||||
* entry
|
||||
*/
|
||||
Entry(int lookupIndex, ZipCentralDirectoryFileHeaderRecord centralRecord) {
|
||||
this.lookupIndex = lookupIndex;
|
||||
this.centralRecord = centralRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the lookup index of the entry. Each entry has a unique lookup index but
|
||||
* they aren't the same as the order that the entry was loaded.
|
||||
* @return the entry lookup index
|
||||
*/
|
||||
public int getLookupIndex() {
|
||||
return this.lookupIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if this is a directory entry.
|
||||
* @return if the entry is a directory
|
||||
*/
|
||||
public boolean isDirectory() {
|
||||
return getName().endsWith("/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this entry has a name starting with the given prefix.
|
||||
* @param prefix the required prefix
|
||||
* @return if the entry name starts with the prefix
|
||||
*/
|
||||
public boolean hasNameStartingWith(CharSequence prefix) {
|
||||
String name = this.name;
|
||||
if (name != null) {
|
||||
return name.startsWith(prefix.toString());
|
||||
}
|
||||
long pos = getCentralDirectoryFileHeaderRecordPos(this.lookupIndex)
|
||||
+ ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET;
|
||||
return ZipString.startsWith(null, ZipContent.this.data, pos, this.centralRecord.fileNameLength(),
|
||||
prefix) != -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of this entry.
|
||||
* @return the entry name
|
||||
*/
|
||||
public String getName() {
|
||||
String name = this.name;
|
||||
if (name == null) {
|
||||
int offset = ZipContent.this.nameOffsetLookups.get(this.lookupIndex);
|
||||
long pos = getCentralDirectoryFileHeaderRecordPos(this.lookupIndex)
|
||||
+ ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET + offset;
|
||||
name = ZipString.readString(ZipContent.this.data, pos, this.centralRecord.fileNameLength() - offset);
|
||||
this.name = name;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the compression method for this entry.
|
||||
* @return the compression method
|
||||
* @see ZipEntry#STORED
|
||||
* @see ZipEntry#DEFLATED
|
||||
*/
|
||||
public int getCompressionMethod() {
|
||||
return this.centralRecord.compressionMethod();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the uncompressed size of this entry.
|
||||
* @return the uncompressed size
|
||||
*/
|
||||
public int getUncompressedSize() {
|
||||
return this.centralRecord.uncompressedSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a {@link DataBlock} providing access to raw contents of the entry (not
|
||||
* including the local file header).
|
||||
* <p>
|
||||
* To release resources, the {@link #close()} method of the data block should be
|
||||
* called explicitly or by try-with-resources.
|
||||
* @return the contents of the entry
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
public CloseableDataBlock openContent() throws IOException {
|
||||
FileChannelDataBlock content = getContent();
|
||||
content.open();
|
||||
return content;
|
||||
}
|
||||
|
||||
private FileChannelDataBlock getContent() throws IOException {
|
||||
FileChannelDataBlock content = this.content;
|
||||
if (content == null) {
|
||||
int pos = this.centralRecord.offsetToLocalHeader();
|
||||
checkNotZip64Extended(pos);
|
||||
ZipLocalFileHeaderRecord localHeader = ZipLocalFileHeaderRecord.load(ZipContent.this.data, pos);
|
||||
int size = this.centralRecord.compressedSize();
|
||||
checkNotZip64Extended(size);
|
||||
content = ZipContent.this.data.slice(pos + localHeader.size(), size);
|
||||
this.content = content;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
private void checkNotZip64Extended(int value) throws IOException {
|
||||
if (value == 0xFFFFFFFF) {
|
||||
throw new IOException("Zip64 extended information extra fields are not supported");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt the raw entry into a {@link ZipEntry} or {@link ZipEntry} subclass.
|
||||
* @param <E> the entry type
|
||||
* @param factory the factory used to create the {@link ZipEntry}
|
||||
* @return a fully populated zip entry
|
||||
*/
|
||||
public <E extends ZipEntry> E as(Function<String, E> factory) {
|
||||
return as((entry, name) -> factory.apply(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt the raw entry into a {@link ZipEntry} or {@link ZipEntry} subclass.
|
||||
* @param <E> the entry type
|
||||
* @param factory the factory used to create the {@link ZipEntry}
|
||||
* @return a fully populated zip entry
|
||||
*/
|
||||
public <E extends ZipEntry> E as(BiFunction<Entry, String, E> factory) {
|
||||
try {
|
||||
E result = factory.apply(this, getName());
|
||||
long pos = getCentralDirectoryFileHeaderRecordPos(this.lookupIndex);
|
||||
this.centralRecord.copyTo(ZipContent.this.data, pos, result);
|
||||
return result;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.zip;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import org.springframework.boot.loader.log.DebugLogger;
|
||||
|
||||
/**
|
||||
* A ZIP File "End of central directory record" (EOCD).
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @param numberOfThisDisk the number of this disk (or 0xffff for Zip64)
|
||||
* @param diskWhereCentralDirectoryStarts the disk where central directory starts (or
|
||||
* 0xffff for Zip64)
|
||||
* @param numberOfCentralDirectoryEntriesOnThisDisk the number of central directory
|
||||
* entries on this disk (or 0xffff for Zip64)
|
||||
* @param totalNumberOfCentralDirectoryEntries the total number of central directory
|
||||
* entries (or 0xffff for Zip64)
|
||||
* @param sizeOfCentralDirectory the size of central directory (bytes) (or 0xffffffff for
|
||||
* Zip64)
|
||||
* @param offsetToStartOfCentralDirectory the offset of start of central directory,
|
||||
* relative to start of archive (or 0xffffffff for Zip64)
|
||||
* @param commentLength the length of the comment field
|
||||
* @see <a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT">Chapter
|
||||
* 4.3.16 of the Zip File Format Specification</a>
|
||||
*/
|
||||
record ZipEndOfCentralDirectoryRecord(short numberOfThisDisk, short diskWhereCentralDirectoryStarts,
|
||||
short numberOfCentralDirectoryEntriesOnThisDisk, short totalNumberOfCentralDirectoryEntries,
|
||||
int sizeOfCentralDirectory, int offsetToStartOfCentralDirectory, short commentLength) {
|
||||
|
||||
ZipEndOfCentralDirectoryRecord(short totalNumberOfCentralDirectoryEntries, int sizeOfCentralDirectory,
|
||||
int offsetToStartOfCentralDirectory) {
|
||||
this((short) 0, (short) 0, totalNumberOfCentralDirectoryEntries, totalNumberOfCentralDirectoryEntries,
|
||||
sizeOfCentralDirectory, offsetToStartOfCentralDirectory, (short) 0);
|
||||
}
|
||||
|
||||
private static final DebugLogger debug = DebugLogger.get(ZipEndOfCentralDirectoryRecord.class);
|
||||
|
||||
private static final int SIGNATURE = 0x06054b50;
|
||||
|
||||
private static final int MAXIMUM_COMMENT_LENGTH = 0xFFFF;
|
||||
|
||||
private static final int MINIMUM_SIZE = 22;
|
||||
|
||||
private static final int MAXIMUM_SIZE = MINIMUM_SIZE + MAXIMUM_COMMENT_LENGTH;
|
||||
|
||||
static final int BUFFER_SIZE = 256;
|
||||
|
||||
/**
|
||||
* The offset of the file comment relative to the record start position.
|
||||
*/
|
||||
static final int COMMENT_OFFSET = MINIMUM_SIZE;
|
||||
|
||||
/**
|
||||
* Return the size of this record.
|
||||
* @return the record size
|
||||
*/
|
||||
long size() {
|
||||
return MINIMUM_SIZE + this.commentLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the contents of this record as a byte array suitable for writing to a zip.
|
||||
* @return the record as a byte array
|
||||
*/
|
||||
byte[] asByteArray() {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(MINIMUM_SIZE);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer.putInt(SIGNATURE);
|
||||
buffer.putShort(this.numberOfThisDisk);
|
||||
buffer.putShort(this.diskWhereCentralDirectoryStarts);
|
||||
buffer.putShort(this.numberOfCentralDirectoryEntriesOnThisDisk);
|
||||
buffer.putShort(this.totalNumberOfCentralDirectoryEntries);
|
||||
buffer.putInt(this.sizeOfCentralDirectory);
|
||||
buffer.putInt(this.offsetToStartOfCentralDirectory);
|
||||
buffer.putShort(this.commentLength);
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ZipEndOfCentralDirectoryRecord} instance from the specified
|
||||
* {@link DataBlock} by searching backwards from the end until a valid record is
|
||||
* located.
|
||||
* @param dataBlock the source data block
|
||||
* @return the {@link Located located} {@link ZipEndOfCentralDirectoryRecord}
|
||||
* @throws IOException if the {@link ZipEndOfCentralDirectoryRecord} cannot be read
|
||||
*/
|
||||
static Located load(DataBlock dataBlock) throws IOException {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
long pos = locate(dataBlock, buffer);
|
||||
return new Located(pos, new ZipEndOfCentralDirectoryRecord(buffer.getShort(), buffer.getShort(),
|
||||
buffer.getShort(), buffer.getShort(), buffer.getInt(), buffer.getInt(), buffer.getShort()));
|
||||
}
|
||||
|
||||
private static long locate(DataBlock dataBlock, ByteBuffer buffer) throws IOException {
|
||||
long endPos = dataBlock.size();
|
||||
debug.log("Finding EndOfCentralDirectoryRecord starting at end position %s", endPos);
|
||||
while (endPos > 0) {
|
||||
buffer.clear();
|
||||
long totalRead = dataBlock.size() - endPos;
|
||||
if (totalRead > MAXIMUM_SIZE) {
|
||||
throw new IOException(
|
||||
"Zip 'End Of Central Directory Record' not found after reading " + totalRead + " bytes");
|
||||
}
|
||||
long startPos = endPos - buffer.limit();
|
||||
if (startPos < 0) {
|
||||
buffer.limit((int) startPos + buffer.limit());
|
||||
startPos = 0;
|
||||
}
|
||||
debug.log("Finding EndOfCentralDirectoryRecord from %s with limit %s", startPos, buffer.limit());
|
||||
dataBlock.readFully(buffer, startPos);
|
||||
int offset = findInBuffer(buffer);
|
||||
if (offset >= 0) {
|
||||
debug.log("Found EndOfCentralDirectoryRecord at %s + %s", startPos, offset);
|
||||
return startPos + offset;
|
||||
}
|
||||
endPos = endPos - BUFFER_SIZE + MINIMUM_SIZE;
|
||||
}
|
||||
throw new IOException("Zip 'End Of Central Directory Record' not found after reading entire data block");
|
||||
}
|
||||
|
||||
private static int findInBuffer(ByteBuffer buffer) {
|
||||
for (int pos = buffer.limit() - 4; pos >= 0; pos--) {
|
||||
buffer.position(pos);
|
||||
if (buffer.getInt() == SIGNATURE) {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* A located {@link ZipEndOfCentralDirectoryRecord}.
|
||||
*
|
||||
* @param pos the position of the record
|
||||
* @param endOfCentralDirectoryRecord the located end of central directory record
|
||||
*/
|
||||
record Located(long pos, ZipEndOfCentralDirectoryRecord endOfCentralDirectoryRecord) {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.zip;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import org.springframework.boot.loader.log.DebugLogger;
|
||||
|
||||
/**
|
||||
* A ZIP File "Local file header record" (LFH).
|
||||
*
|
||||
* @param versionNeededToExtract the version needed to extract the zip
|
||||
* @param generalPurposeBitFlag the general purpose bit flag
|
||||
* @param compressionMethod the compression method used for this entry
|
||||
* @param lastModFileTime the last modified file time
|
||||
* @param lastModFileDate the last modified file date
|
||||
* @param crc32 the CRC32 checksum
|
||||
* @param compressedSize the size of the entry when compressed
|
||||
* @param uncompressedSize the size of the entry when uncompressed
|
||||
* @param fileNameLength the file name length
|
||||
* @param extraFieldLength the extra field length
|
||||
* @author Phillip Webb
|
||||
* @see <a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT">Chapter
|
||||
* 4.3.7 of the Zip File Format Specification</a>
|
||||
*/
|
||||
record ZipLocalFileHeaderRecord(short versionNeededToExtract, short generalPurposeBitFlag, short compressionMethod,
|
||||
short lastModFileTime, short lastModFileDate, int crc32, int compressedSize, int uncompressedSize,
|
||||
short fileNameLength, short extraFieldLength) {
|
||||
|
||||
private static final DebugLogger debug = DebugLogger.get(ZipLocalFileHeaderRecord.class);
|
||||
|
||||
private static final int SIGNATURE = 0x04034b50;
|
||||
|
||||
private static final int MINIMUM_SIZE = 30;
|
||||
|
||||
/**
|
||||
* Return the size of this record.
|
||||
* @return the record size
|
||||
*/
|
||||
long size() {
|
||||
return MINIMUM_SIZE + fileNameLength() + extraFieldLength();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ZipLocalFileHeaderRecord} with a new
|
||||
* {@link #extraFieldLength()}.
|
||||
* @param extraFieldLength the new extra field length
|
||||
* @return a new {@link ZipLocalFileHeaderRecord} instance
|
||||
*/
|
||||
ZipLocalFileHeaderRecord withExtraFieldLength(short extraFieldLength) {
|
||||
return new ZipLocalFileHeaderRecord(this.versionNeededToExtract, this.generalPurposeBitFlag,
|
||||
this.compressionMethod, this.lastModFileTime, this.lastModFileDate, this.crc32, this.compressedSize,
|
||||
this.uncompressedSize, this.fileNameLength, extraFieldLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link ZipLocalFileHeaderRecord} with a new {@link #fileNameLength()}.
|
||||
* @param fileNameLength the new file name length
|
||||
* @return a new {@link ZipLocalFileHeaderRecord} instance
|
||||
*/
|
||||
ZipLocalFileHeaderRecord withFileNameLength(short fileNameLength) {
|
||||
return new ZipLocalFileHeaderRecord(this.versionNeededToExtract, this.generalPurposeBitFlag,
|
||||
this.compressionMethod, this.lastModFileTime, this.lastModFileDate, this.crc32, this.compressedSize,
|
||||
this.uncompressedSize, fileNameLength, this.extraFieldLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the contents of this record as a byte array suitable for writing to a zip.
|
||||
* @return the record as a byte array
|
||||
*/
|
||||
byte[] asByteArray() {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(MINIMUM_SIZE);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer.putInt(SIGNATURE);
|
||||
buffer.putShort(this.versionNeededToExtract);
|
||||
buffer.putShort(this.generalPurposeBitFlag);
|
||||
buffer.putShort(this.compressionMethod);
|
||||
buffer.putShort(this.lastModFileTime);
|
||||
buffer.putShort(this.lastModFileDate);
|
||||
buffer.putInt(this.crc32);
|
||||
buffer.putInt(this.compressedSize);
|
||||
buffer.putInt(this.uncompressedSize);
|
||||
buffer.putShort(this.fileNameLength);
|
||||
buffer.putShort(this.extraFieldLength);
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the {@link ZipLocalFileHeaderRecord} from the given data block.
|
||||
* @param dataBlock the source data block
|
||||
* @param pos the position of the record
|
||||
* @return a new {@link ZipLocalFileHeaderRecord} instance
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
static ZipLocalFileHeaderRecord load(DataBlock dataBlock, long pos) throws IOException {
|
||||
debug.log("Loading LocalFileHeaderRecord from position %s", pos);
|
||||
ByteBuffer buffer = ByteBuffer.allocate(MINIMUM_SIZE);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
dataBlock.readFully(buffer, pos);
|
||||
buffer.rewind();
|
||||
if (buffer.getInt() != SIGNATURE) {
|
||||
throw new IOException("Zip 'Local File Header Record' not found at position " + pos);
|
||||
}
|
||||
return new ZipLocalFileHeaderRecord(buffer.getShort(), buffer.getShort(), buffer.getShort(), buffer.getShort(),
|
||||
buffer.getShort(), buffer.getInt(), buffer.getInt(), buffer.getInt(), buffer.getShort(),
|
||||
buffer.getShort());
|
||||
}
|
||||
}
|
@ -0,0 +1,320 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.zip;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.springframework.boot.loader.log.DebugLogger;
|
||||
|
||||
/**
|
||||
* Internal utility class for working with the string content of zip records. Provides
|
||||
* methods that work with raw bytes to save creating temporary strings.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
final class ZipString {
|
||||
|
||||
private static final DebugLogger debug = DebugLogger.get(ZipString.class);
|
||||
|
||||
static final int BUFFER_SIZE = 256;
|
||||
|
||||
private static final int[] INITIAL_BYTE_BITMASK = { 0x7F, 0x1F, 0x0F, 0x07 };
|
||||
|
||||
private static final int SUBSEQUENT_BYTE_BITMASK = 0x3F;
|
||||
|
||||
private static final int EMPTY_HASH = "".hashCode();
|
||||
|
||||
private static final int EMPTY_SLASH_HASH = "/".hashCode();
|
||||
|
||||
private ZipString() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a hash for a char sequence, optionally appending '/'.
|
||||
* @param charSequence the source char sequence
|
||||
* @param addEndSlash if slash should be added to the string if it's not already
|
||||
* present
|
||||
* @return the hash
|
||||
*/
|
||||
static int hash(CharSequence charSequence, boolean addEndSlash) {
|
||||
return hash(0, charSequence, addEndSlash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a hash for a char sequence, optionally appending '/'.
|
||||
* @param initialHash the initial hash value
|
||||
* @param charSequence the source char sequence
|
||||
* @param addEndSlash if slash should be added to the string if it's not already
|
||||
* present
|
||||
* @return the hash
|
||||
*/
|
||||
static int hash(int initialHash, CharSequence charSequence, boolean addEndSlash) {
|
||||
if (charSequence == null || charSequence.isEmpty()) {
|
||||
return (!addEndSlash) ? EMPTY_HASH : EMPTY_SLASH_HASH;
|
||||
}
|
||||
boolean endsWithSlash = charSequence.charAt(charSequence.length() - 1) == '/';
|
||||
int hash = initialHash;
|
||||
if (charSequence instanceof String && initialHash == 0) {
|
||||
// We're compatible with String.hashCode and it might be already calculated
|
||||
hash = charSequence.hashCode();
|
||||
}
|
||||
else {
|
||||
for (int i = 0; i < charSequence.length(); i++) {
|
||||
char ch = charSequence.charAt(i);
|
||||
hash = 31 * hash + ch;
|
||||
}
|
||||
}
|
||||
hash = (addEndSlash && !endsWithSlash) ? 31 * hash + '/' : hash;
|
||||
debug.log("%s calculated for charsequence '%s' (addEndSlash=%s)", hash, charSequence, endsWithSlash);
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a hash for bytes read from a {@link DataBlock}, optionally appending '/'.
|
||||
* @param buffer the buffer to use or {@code null}
|
||||
* @param dataBlock the source data block
|
||||
* @param pos the position in the data block where the string starts
|
||||
* @param len the number of bytes to read from the block
|
||||
* @param addEndSlash if slash should be added to the string if it's not already
|
||||
* present
|
||||
* @return the hash
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
static int hash(ByteBuffer buffer, DataBlock dataBlock, long pos, int len, boolean addEndSlash) throws IOException {
|
||||
if (len == 0) {
|
||||
return (!addEndSlash) ? EMPTY_HASH : EMPTY_SLASH_HASH;
|
||||
}
|
||||
buffer = (buffer != null) ? buffer : ByteBuffer.allocate(BUFFER_SIZE);
|
||||
byte[] bytes = buffer.array();
|
||||
int hash = 0;
|
||||
char lastChar = 0;
|
||||
while (len > 0) {
|
||||
int count = readInBuffer(dataBlock, pos, buffer, len);
|
||||
len -= count;
|
||||
pos += count;
|
||||
for (int byteIndex = 0; byteIndex < count;) {
|
||||
int codePointSize = getCodePointSize(bytes, byteIndex);
|
||||
if (!hasEnoughBytes(byteIndex, codePointSize, count)) {
|
||||
pos--;
|
||||
len++;
|
||||
break;
|
||||
}
|
||||
int codePoint = getCodePoint(bytes, byteIndex, codePointSize);
|
||||
byteIndex += codePointSize;
|
||||
if (codePoint <= 0xFFFF) {
|
||||
lastChar = (char) (codePoint & 0xFFFF);
|
||||
hash = 31 * hash + lastChar;
|
||||
}
|
||||
else {
|
||||
lastChar = 0;
|
||||
hash = 31 * hash + Character.highSurrogate(codePoint);
|
||||
hash = 31 * hash + Character.lowSurrogate(codePoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
hash = (addEndSlash && lastChar != '/') ? 31 * hash + '/' : hash;
|
||||
debug.log("%08X calculated for datablock position %s size %s (addEndSlash=%s)", hash, pos, len, addEndSlash);
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if the bytes read from a {@link DataBlock} matches the give
|
||||
* {@link CharSequence}.
|
||||
* @param buffer the buffer to use or {@code null}
|
||||
* @param dataBlock the source data block
|
||||
* @param pos the position in the data block where the string starts
|
||||
* @param len the number of bytes to read from the block
|
||||
* @param charSequence the char sequence with which to compare
|
||||
* @param addSlash also accept {@code charSequence + '/'} when it doesn't already end
|
||||
* with one
|
||||
* @return true if the contents are considered equal
|
||||
*/
|
||||
static boolean matches(ByteBuffer buffer, DataBlock dataBlock, long pos, int len, CharSequence charSequence,
|
||||
boolean addSlash) {
|
||||
if (charSequence.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
buffer = (buffer != null) ? buffer : ByteBuffer.allocate(BUFFER_SIZE);
|
||||
try {
|
||||
return compare(buffer, dataBlock, pos, len, charSequence,
|
||||
(!addSlash) ? CompareType.MATCHES : CompareType.MATCHES_ADDING_SLASH) != -1;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the bytes read from a {@link DataBlock} starts with the given
|
||||
* {@link CharSequence}.
|
||||
* @param buffer the buffer to use or {@code null}
|
||||
* @param dataBlock the source data block
|
||||
* @param pos the position in the data block where the string starts
|
||||
* @param len the number of bytes to read from the block
|
||||
* @param charSequence the required starting chars
|
||||
* @return {@code -1} if the data block does not start with the char sequence, or a
|
||||
* positive number indicating the number of bytes that contain the starting chars
|
||||
*/
|
||||
static int startsWith(ByteBuffer buffer, DataBlock dataBlock, long pos, int len, CharSequence charSequence) {
|
||||
if (charSequence.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
buffer = (buffer != null) ? buffer : ByteBuffer.allocate(BUFFER_SIZE);
|
||||
try {
|
||||
return compare(buffer, dataBlock, pos, len, charSequence, CompareType.STARTS_WITH);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static int compare(ByteBuffer buffer, DataBlock dataBlock, long pos, int len, CharSequence charSequence,
|
||||
CompareType compareType) throws IOException {
|
||||
if (charSequence.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
boolean addSlash = compareType == CompareType.MATCHES_ADDING_SLASH && !endsWith(charSequence, '/');
|
||||
int charSequenceIndex = 0;
|
||||
int maxCharSequenceLength = (!addSlash) ? charSequence.length() : charSequence.length() + 1;
|
||||
int result = 0;
|
||||
byte[] bytes = buffer.array();
|
||||
while (len > 0) {
|
||||
int count = readInBuffer(dataBlock, pos, buffer, len);
|
||||
len -= count;
|
||||
pos += count;
|
||||
for (int byteIndex = 0; byteIndex < count;) {
|
||||
int codePointSize = getCodePointSize(bytes, byteIndex);
|
||||
if (!hasEnoughBytes(byteIndex, codePointSize, count)) {
|
||||
pos--;
|
||||
len++;
|
||||
break;
|
||||
}
|
||||
int codePoint = getCodePoint(bytes, byteIndex, codePointSize);
|
||||
result += codePointSize;
|
||||
if (codePoint <= 0xFFFF) {
|
||||
char ch = (char) (codePoint & 0xFFFF);
|
||||
if (charSequenceIndex >= maxCharSequenceLength
|
||||
|| getChar(charSequence, charSequenceIndex++) != ch) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
char ch = Character.highSurrogate(codePoint);
|
||||
if (charSequenceIndex >= maxCharSequenceLength
|
||||
|| getChar(charSequence, charSequenceIndex++) != ch) {
|
||||
return -1;
|
||||
}
|
||||
ch = Character.lowSurrogate(codePoint);
|
||||
if (charSequenceIndex >= charSequence.length()
|
||||
|| getChar(charSequence, charSequenceIndex++) != ch) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if (compareType == CompareType.STARTS_WITH && charSequenceIndex >= charSequence.length()) {
|
||||
return result;
|
||||
}
|
||||
byteIndex += codePointSize;
|
||||
}
|
||||
}
|
||||
return (charSequenceIndex >= charSequence.length()) ? result : -1;
|
||||
}
|
||||
|
||||
private static boolean hasEnoughBytes(int byteIndex, int codePointSize, int count) {
|
||||
return (byteIndex + codePointSize - 1) < count;
|
||||
}
|
||||
|
||||
private static boolean endsWith(CharSequence charSequence, char ch) {
|
||||
return !charSequence.isEmpty() && charSequence.charAt(charSequence.length() - 1) == ch;
|
||||
}
|
||||
|
||||
private static char getChar(CharSequence charSequence, int index) {
|
||||
return (index != charSequence.length()) ? charSequence.charAt(index) : '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a string value from the given data block.
|
||||
* @param data the source data
|
||||
* @param pos the position to read from
|
||||
* @param len the number of bytes to read
|
||||
* @return the contents as a string
|
||||
*/
|
||||
static String readString(DataBlock data, long pos, long len) {
|
||||
try {
|
||||
if (len > Integer.MAX_VALUE) {
|
||||
throw new IllegalStateException("String is too long to read");
|
||||
}
|
||||
ByteBuffer buffer = ByteBuffer.allocate((int) len);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
data.readFully(buffer, pos);
|
||||
return new String(buffer.array(), StandardCharsets.UTF_8);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static int readInBuffer(DataBlock dataBlock, long pos, ByteBuffer buffer, int maxLen) throws IOException {
|
||||
buffer.clear();
|
||||
if (buffer.remaining() > maxLen) {
|
||||
buffer.limit(maxLen);
|
||||
}
|
||||
int count = dataBlock.read(buffer, pos);
|
||||
if (count <= 0) {
|
||||
throw new EOFException();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static int getCodePointSize(byte[] bytes, int i) {
|
||||
int b = bytes[i] & 0xFF;
|
||||
if ((b & 0b1_0000000) == 0b0_0000000) {
|
||||
return 1;
|
||||
}
|
||||
if ((b & 0b111_00000) == 0b110_00000) {
|
||||
return 2;
|
||||
}
|
||||
if ((b & 0b1111_0000) == 0b1110_0000) {
|
||||
return 3;
|
||||
}
|
||||
return 4;
|
||||
}
|
||||
|
||||
private static int getCodePoint(byte[] bytes, int i, int codePointSize) {
|
||||
int codePoint = bytes[i] & 0xFF;
|
||||
codePoint &= INITIAL_BYTE_BITMASK[codePointSize - 1];
|
||||
for (int j = 1; j < codePointSize; j++) {
|
||||
codePoint = (codePoint << 6) + (bytes[i + j] & SUBSEQUENT_BYTE_BITMASK);
|
||||
}
|
||||
return codePoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported compare types.
|
||||
*/
|
||||
private enum CompareType {
|
||||
|
||||
MATCHES, MATCHES_ADDING_SLASH, STARTS_WITH
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides low-level support for handling zip content, including support for nested and
|
||||
* virtual zip files.
|
||||
*/
|
||||
package org.springframework.boot.loader.zip;
|
@ -1,111 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.net.JarURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.loader.jar.JarFile;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link LaunchedURLClassLoader}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@SuppressWarnings("resource")
|
||||
class LaunchedURLClassLoaderTests {
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
@Test
|
||||
void resolveResourceFromArchive() throws Exception {
|
||||
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(
|
||||
new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader());
|
||||
assertThat(loader.getResource("demo/Application.java")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveResourcesFromArchive() throws Exception {
|
||||
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(
|
||||
new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader());
|
||||
assertThat(loader.getResources("demo/Application.java").hasMoreElements()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveRootPathFromArchive() throws Exception {
|
||||
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(
|
||||
new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader());
|
||||
assertThat(loader.getResource("")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveRootResourcesFromArchive() throws Exception {
|
||||
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(
|
||||
new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader());
|
||||
assertThat(loader.getResources("").hasMoreElements()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveFromNested() throws Exception {
|
||||
File file = new File(this.tempDir, "test.jar");
|
||||
TestJarCreator.createTestJar(file);
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
URL url = jarFile.getUrl();
|
||||
try (LaunchedURLClassLoader loader = new LaunchedURLClassLoader(new URL[] { url }, null)) {
|
||||
URL resource = loader.getResource("nested.jar!/3.dat");
|
||||
assertThat(resource).hasToString(url + "nested.jar!/3.dat");
|
||||
try (InputStream input = resource.openConnection().getInputStream()) {
|
||||
assertThat(input.read()).isEqualTo(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveFromNestedWhileThreadIsInterrupted() throws Exception {
|
||||
File file = new File(this.tempDir, "test.jar");
|
||||
TestJarCreator.createTestJar(file);
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
URL url = jarFile.getUrl();
|
||||
try (LaunchedURLClassLoader loader = new LaunchedURLClassLoader(new URL[] { url }, null)) {
|
||||
Thread.currentThread().interrupt();
|
||||
URL resource = loader.getResource("nested.jar!/3.dat");
|
||||
assertThat(resource).hasToString(url + "nested.jar!/3.dat");
|
||||
URLConnection connection = resource.openConnection();
|
||||
try (InputStream input = connection.getInputStream()) {
|
||||
assertThat(input.read()).isEqualTo(3);
|
||||
}
|
||||
((JarURLConnection) connection).getJarFile().close();
|
||||
}
|
||||
finally {
|
||||
Thread.interrupted();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,189 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.archive;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.loader.TestJarCreator;
|
||||
import org.springframework.boot.loader.archive.Archive.Entry;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ExplodedArchive}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class ExplodedArchiveTests {
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
private File rootDirectory;
|
||||
|
||||
private ExplodedArchive archive;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
createArchive();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws Exception {
|
||||
if (this.archive != null) {
|
||||
this.archive.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void createArchive() throws Exception {
|
||||
createArchive(null);
|
||||
}
|
||||
|
||||
private void createArchive(String directoryName) throws Exception {
|
||||
File file = new File(this.tempDir, "test.jar");
|
||||
TestJarCreator.createTestJar(file);
|
||||
this.rootDirectory = (StringUtils.hasText(directoryName) ? new File(this.tempDir, directoryName)
|
||||
: new File(this.tempDir, UUID.randomUUID().toString()));
|
||||
JarFile jarFile = new JarFile(file);
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
File destination = new File(this.rootDirectory.getAbsolutePath() + File.separator + entry.getName());
|
||||
destination.getParentFile().mkdirs();
|
||||
if (entry.isDirectory()) {
|
||||
destination.mkdir();
|
||||
}
|
||||
else {
|
||||
FileCopyUtils.copy(jarFile.getInputStream(entry), new FileOutputStream(destination));
|
||||
}
|
||||
}
|
||||
this.archive = new ExplodedArchive(this.rootDirectory);
|
||||
jarFile.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getManifest() throws Exception {
|
||||
assertThat(this.archive.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getEntries() {
|
||||
Map<String, Archive.Entry> entries = getEntriesMap(this.archive);
|
||||
assertThat(entries).hasSize(12);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUrl() throws Exception {
|
||||
assertThat(this.archive.getUrl()).isEqualTo(this.rootDirectory.toURI().toURL());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUrlWithSpaceInPath() throws Exception {
|
||||
createArchive("spaces in the name");
|
||||
assertThat(this.archive.getUrl()).isEqualTo(this.rootDirectory.toURI().toURL());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNestedArchive() throws Exception {
|
||||
Entry entry = getEntriesMap(this.archive).get("nested.jar");
|
||||
Archive nested = this.archive.getNestedArchive(entry);
|
||||
assertThat(nested.getUrl()).hasToString(this.rootDirectory.toURI() + "nested.jar");
|
||||
nested.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void nestedDirArchive() throws Exception {
|
||||
Entry entry = getEntriesMap(this.archive).get("d/");
|
||||
Archive nested = this.archive.getNestedArchive(entry);
|
||||
Map<String, Entry> nestedEntries = getEntriesMap(nested);
|
||||
assertThat(nestedEntries).hasSize(1);
|
||||
assertThat(nested.getUrl()).hasToString("file:" + this.rootDirectory.toURI().getPath() + "d/");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNonRecursiveEntriesForRoot() throws Exception {
|
||||
try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("/"), false)) {
|
||||
Map<String, Archive.Entry> entries = getEntriesMap(explodedArchive);
|
||||
assertThat(entries).hasSizeGreaterThan(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNonRecursiveManifest() throws Exception {
|
||||
try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"))) {
|
||||
assertThat(explodedArchive.getManifest()).isNotNull();
|
||||
Map<String, Archive.Entry> entries = getEntriesMap(explodedArchive);
|
||||
assertThat(entries).hasSize(4);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNonRecursiveManifestEvenIfNonRecursive() throws Exception {
|
||||
try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"), false)) {
|
||||
assertThat(explodedArchive.getManifest()).isNotNull();
|
||||
Map<String, Archive.Entry> entries = getEntriesMap(explodedArchive);
|
||||
assertThat(entries).hasSize(3);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getResourceAsStream() throws Exception {
|
||||
try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"))) {
|
||||
assertThat(explodedArchive.getManifest()).isNotNull();
|
||||
URLClassLoader loader = new URLClassLoader(new URL[] { explodedArchive.getUrl() });
|
||||
assertThat(loader.getResourceAsStream("META-INF/spring/application.xml")).isNotNull();
|
||||
loader.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getResourceAsStreamNonRecursive() throws Exception {
|
||||
try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"), false)) {
|
||||
assertThat(explodedArchive.getManifest()).isNotNull();
|
||||
URLClassLoader loader = new URLClassLoader(new URL[] { explodedArchive.getUrl() });
|
||||
assertThat(loader.getResourceAsStream("META-INF/spring/application.xml")).isNotNull();
|
||||
loader.close();
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Archive.Entry> getEntriesMap(Archive archive) {
|
||||
Map<String, Archive.Entry> entries = new HashMap<>();
|
||||
for (Archive.Entry entry : archive) {
|
||||
entries.put(entry.getName(), entry);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
}
|
@ -1,207 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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 org.springframework.boot.loader.archive;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.loader.TestJarCreator;
|
||||
import org.springframework.boot.loader.archive.Archive.Entry;
|
||||
import org.springframework.boot.loader.jar.JarFile;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link JarFileArchive}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @author Camille Vienot
|
||||
*/
|
||||
class JarFileArchiveTests {
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
private File rootJarFile;
|
||||
|
||||
private JarFileArchive archive;
|
||||
|
||||
private String rootJarFileUrl;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
setup(false);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws Exception {
|
||||
this.archive.close();
|
||||
}
|
||||
|
||||
private void setup(boolean unpackNested) throws Exception {
|
||||
this.rootJarFile = new File(this.tempDir, "root.jar");
|
||||
this.rootJarFileUrl = this.rootJarFile.toURI().toString();
|
||||
TestJarCreator.createTestJar(this.rootJarFile, unpackNested);
|
||||
if (this.archive != null) {
|
||||
this.archive.close();
|
||||
}
|
||||
this.archive = new JarFileArchive(this.rootJarFile);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getManifest() throws Exception {
|
||||
assertThat(this.archive.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getEntries() {
|
||||
Map<String, Archive.Entry> entries = getEntriesMap(this.archive);
|
||||
assertThat(entries).hasSize(12);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUrl() throws Exception {
|
||||
URL url = this.archive.getUrl();
|
||||
assertThat(url).hasToString(this.rootJarFileUrl);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNestedArchive() throws Exception {
|
||||
Entry entry = getEntriesMap(this.archive).get("nested.jar");
|
||||
try (Archive nested = this.archive.getNestedArchive(entry)) {
|
||||
assertThat(nested.getUrl()).hasToString("jar:" + this.rootJarFileUrl + "!/nested.jar!/");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNestedUnpackedArchive() throws Exception {
|
||||
setup(true);
|
||||
Entry entry = getEntriesMap(this.archive).get("nested.jar");
|
||||
try (Archive nested = this.archive.getNestedArchive(entry)) {
|
||||
assertThat(nested.getUrl().toString()).startsWith("file:");
|
||||
assertThat(nested.getUrl().toString()).endsWith("/nested.jar");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void unpackedLocationsAreUniquePerArchive() throws Exception {
|
||||
setup(true);
|
||||
Entry entry = getEntriesMap(this.archive).get("nested.jar");
|
||||
URL firstNestedUrl;
|
||||
try (Archive firstNested = this.archive.getNestedArchive(entry)) {
|
||||
firstNestedUrl = firstNested.getUrl();
|
||||
}
|
||||
this.archive.close();
|
||||
setup(true);
|
||||
entry = getEntriesMap(this.archive).get("nested.jar");
|
||||
try (Archive secondNested = this.archive.getNestedArchive(entry)) {
|
||||
URL secondNestedUrl = secondNested.getUrl();
|
||||
assertThat(secondNestedUrl).isNotEqualTo(firstNestedUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void unpackedLocationsFromSameArchiveShareSameParent() throws Exception {
|
||||
setup(true);
|
||||
try (Archive nestedArchive = this.archive.getNestedArchive(getEntriesMap(this.archive).get("nested.jar"));
|
||||
Archive anotherNestedArchive = this.archive
|
||||
.getNestedArchive(getEntriesMap(this.archive).get("another-nested.jar"))) {
|
||||
File nested = new File(nestedArchive.getUrl().toURI());
|
||||
File anotherNested = new File(anotherNestedArchive.getUrl().toURI());
|
||||
assertThat(nested).hasParent(anotherNested.getParent());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void filesInZip64ArchivesAreAllListed() throws IOException {
|
||||
File file = new File(this.tempDir, "test.jar");
|
||||
FileCopyUtils.copy(writeZip64Jar(), file);
|
||||
try (JarFileArchive zip64Archive = new JarFileArchive(file)) {
|
||||
@SuppressWarnings("deprecation")
|
||||
Iterator<Entry> entries = zip64Archive.iterator();
|
||||
for (int i = 0; i < 65537; i++) {
|
||||
assertThat(entries.hasNext()).as(i + "nth file is present").isTrue();
|
||||
entries.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void nestedZip64ArchivesAreHandledGracefully() throws Exception {
|
||||
File file = new File(this.tempDir, "test.jar");
|
||||
try (JarOutputStream output = new JarOutputStream(new FileOutputStream(file))) {
|
||||
JarEntry zip64JarEntry = new JarEntry("nested/zip64.jar");
|
||||
output.putNextEntry(zip64JarEntry);
|
||||
byte[] zip64JarData = writeZip64Jar();
|
||||
zip64JarEntry.setSize(zip64JarData.length);
|
||||
zip64JarEntry.setCompressedSize(zip64JarData.length);
|
||||
zip64JarEntry.setMethod(ZipEntry.STORED);
|
||||
CRC32 crc32 = new CRC32();
|
||||
crc32.update(zip64JarData);
|
||||
zip64JarEntry.setCrc(crc32.getValue());
|
||||
output.write(zip64JarData);
|
||||
output.closeEntry();
|
||||
}
|
||||
try (JarFile jarFile = new JarFile(file)) {
|
||||
ZipEntry nestedEntry = jarFile.getEntry("nested/zip64.jar");
|
||||
try (JarFile nestedJarFile = jarFile.getNestedJarFile(nestedEntry)) {
|
||||
Iterator<JarEntry> iterator = nestedJarFile.iterator();
|
||||
for (int i = 0; i < 65537; i++) {
|
||||
assertThat(iterator.hasNext()).as(i + "nth file is present").isTrue();
|
||||
iterator.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] writeZip64Jar() throws IOException {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
try (JarOutputStream jarOutput = new JarOutputStream(bytes)) {
|
||||
for (int i = 0; i < 65537; i++) {
|
||||
jarOutput.putNextEntry(new JarEntry(i + ".dat"));
|
||||
jarOutput.closeEntry();
|
||||
}
|
||||
}
|
||||
return bytes.toByteArray();
|
||||
}
|
||||
|
||||
private Map<String, Archive.Entry> getEntriesMap(Archive archive) {
|
||||
Map<String, Archive.Entry> entries = new HashMap<>();
|
||||
for (Archive.Entry entry : archive) {
|
||||
entries.put(entry.getName(), entry);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue