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
Phillip Webb 1 year ago
parent 75ddb9fa47
commit 7ad4a9817d

@ -1,7 +1,7 @@
[[appendix.executable-jar.jarfile-class]]
== Spring Boot's "`JarFile`" Class
The core class used to support loading nested jars is `org.springframework.boot.loader.jar.JarFile`.
It lets you load jar content from a standard jar file or from nested child jar data.
== Spring Boot's "`NestedJarFile`" Class
The core class used to support loading nested jars is `org.springframework.boot.loader.jar.NestedJarFile`.
It lets you load jar content from nested child jar data.
When first loaded, the location of each `JarEntry` is mapped to a physical file offset of the outer jar, as shown in the following example:
[indent=0]
@ -28,5 +28,7 @@ We do not need to unpack the archive, and we do not need to read all entry data
[[appendix.executable-jar.jarfile-class.compatibility]]
=== Compatibility With the Standard Java "`JarFile`"
Spring Boot Loader strives to remain compatible with existing code and libraries.
`org.springframework.boot.loader.jar.JarFile` extends from `java.util.jar.JarFile` and should work as a drop-in replacement.
The `getURL()` method returns a `URL` that opens a connection compatible with `java.net.JarURLConnection` and can be used with Java's `URLClassLoader`.
`org.springframework.boot.loader.jar.NestedJarFile` extends from `java.util.jar.JarFile` and should work as a drop-in replacement.
Nested JAR URLs of the form `jar:nested:/path/myjar.jar/!BOOT-INF/lib/mylib.jar!/B.class` are supported and open a connection compatible with `java.net.JarURLConnection`.
These can be used with Java's `URLClassLoader`.

@ -1,13 +1,14 @@
[[appendix.executable-jar.launching]]
== Launching Executable Jars
The `org.springframework.boot.loader.Launcher` class is a special bootstrap class that is used as an executable jar's main entry point.
It is the actual `Main-Class` in your jar file, and it is used to setup an appropriate `URLClassLoader` and ultimately call your `main()` method.
The `org.springframework.boot.loader.launch.Launcher` class is a special bootstrap class that is used as an executable jar's main entry point.
It is the actual `Main-Class` in your jar file, and it is used to setup an appropriate `ClassLoader` and ultimately call your `main()` method.
There are three launcher subclasses (`JarLauncher`, `WarLauncher`, and `PropertiesLauncher`).
Their purpose is to load resources (`.class` files and so on) from nested jar files or war files in directories (as opposed to those explicitly on the classpath).
In the case of `JarLauncher` and `WarLauncher`, the nested paths are fixed.
`JarLauncher` looks in `BOOT-INF/lib/`, and `WarLauncher` looks in `WEB-INF/lib/` and `WEB-INF/lib-provided/`.
You can add extra jars in those locations if you want more.
The `PropertiesLauncher` looks in `BOOT-INF/lib/` in your application archive by default.
You can add additional locations by setting an environment variable called `LOADER_PATH` or `loader.path` in `loader.properties` (which is a comma-separated list of directories, archives, or directories within archives).
@ -30,7 +31,7 @@ For a war file, it would be as follows:
[indent=0]
----
Main-Class: org.springframework.boot.loader.WarLauncher
Main-Class: org.springframework.boot.loader.launch.WarLauncher
Start-Class: com.mycompany.project.MyApplication
----

@ -256,7 +256,8 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
this.task.getMainClass().set("com.example.Main");
executeTask();
try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) {
assertThat(jarFile.getEntry("org/springframework/boot/loader/LaunchedURLClassLoader.class")).isNotNull();
assertThat(jarFile.getEntry("org/springframework/boot/loader/launch/LaunchedClassLoader.class"))
.isNotNull();
assertThat(jarFile.getEntry("org/springframework/boot/loader/")).isNotNull();
}
// gh-16698
@ -275,7 +276,8 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
.getAttributes()
.put("Main-Class", "org.springframework.boot.loader.launch.PropertiesLauncher");
try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) {
assertThat(jarFile.getEntry("org/springframework/boot/loader/LaunchedURLClassLoader.class")).isNotNull();
assertThat(jarFile.getEntry("org/springframework/boot/loader/launch/LaunchedClassLoader.class"))
.isNotNull();
assertThat(jarFile.getEntry("org/springframework/boot/loader/")).isNotNull();
}
}

@ -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);
}
}

@ -24,27 +24,32 @@ import java.util.zip.InflaterInputStream;
/**
* {@link InflaterInputStream} that supports the writing of an extra "dummy" byte (which
* is required with JDK 6) and returns accurate available() results.
* is required when using an {@link Inflater} with {@code nowrap}) and returns accurate
* available() results.
*
* @author Phillip Webb
*/
class ZipInflaterInputStream extends InflaterInputStream {
abstract class ZipInflaterInputStream extends InflaterInputStream {
private int available;
private boolean extraBytesWritten;
ZipInflaterInputStream(InputStream inputStream, int size) {
super(inputStream, new Inflater(true), getInflaterBufferSize(size));
ZipInflaterInputStream(InputStream inputStream, Inflater inflater, int size) {
super(inputStream, inflater, getInflaterBufferSize(size));
this.available = size;
}
private static int getInflaterBufferSize(long size) {
size += 2; // inflater likes some space
size = (size > 65536) ? 8192 : size;
size = (size <= 0) ? 4096 : size;
return (int) size;
}
@Override
public int available() throws IOException {
if (this.available < 0) {
return super.available();
}
return this.available;
return (this.available >= 0) ? this.available : super.available();
}
@Override
@ -56,12 +61,6 @@ class ZipInflaterInputStream extends InflaterInputStream {
return result;
}
@Override
public void close() throws IOException {
super.close();
this.inf.end();
}
@Override
protected void fill() throws IOException {
try {
@ -78,11 +77,4 @@ class ZipInflaterInputStream extends InflaterInputStream {
}
}
private static int getInflaterBufferSize(long size) {
size += 2; // inflater likes some space
size = (size > 65536) ? 8192 : size;
size = (size <= 0) ? 4096 : size;
return (int) size;
}
}

@ -15,6 +15,7 @@
*/
/**
* Support for loading and manipulating JAR/WAR files.
* Alternative {@link java.util.jar.JarFile} implementation with support for nested jars.
* @see org.springframework.boot.loader.jar.NestedJarFile
*/
package org.springframework.boot.loader.jar;

@ -16,7 +16,5 @@
/**
* Support for launching the JAR using jarmode.
*
* @see org.springframework.boot.loader.jarmode.JarModeLauncher
*/
package org.springframework.boot.loader.jarmode;

@ -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();
}
}

@ -14,24 +14,20 @@
* limitations under the License.
*/
package org.springframework.boot.loader;
package org.springframework.boot.loader.launch;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.nio.file.Files;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* A class path index file that provides ordering information for JARs.
* A class path index file that provides an ordered classpath for exploded JARs.
*
* @author Madhura Bhave
* @author Phillip Webb
@ -40,11 +36,11 @@ final class ClassPathIndexFile {
private final File root;
private final List<String> lines;
private final Set<String> lines;
private ClassPathIndexFile(File root, List<String> lines) {
this.root = root;
this.lines = lines.stream().map(this::extractName).toList();
this.lines = lines.stream().map(this::extractName).collect(Collectors.toCollection(LinkedHashSet::new));
}
private String extractName(String line) {
@ -78,46 +74,23 @@ final class ClassPathIndexFile {
}
}
static ClassPathIndexFile loadIfPossible(URL root, String location) throws IOException {
return loadIfPossible(asFile(root), location);
}
private static ClassPathIndexFile loadIfPossible(File root, String location) throws IOException {
static ClassPathIndexFile loadIfPossible(File root, String location) throws IOException {
return loadIfPossible(root, new File(root, location));
}
private static ClassPathIndexFile loadIfPossible(File root, File indexFile) throws IOException {
if (indexFile.exists() && indexFile.isFile()) {
try (InputStream inputStream = new FileInputStream(indexFile)) {
return new ClassPathIndexFile(root, loadLines(inputStream));
}
List<String> lines = Files.readAllLines(indexFile.toPath())
.stream()
.filter(ClassPathIndexFile::lineHasText)
.toList();
return new ClassPathIndexFile(root, lines);
}
return null;
}
private static List<String> loadLines(InputStream inputStream) throws IOException {
List<String> lines = new ArrayList<>();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String line = reader.readLine();
while (line != null) {
if (!line.trim().isEmpty()) {
lines.add(line);
}
line = reader.readLine();
}
return Collections.unmodifiableList(lines);
}
private static File asFile(URL url) {
if (!"file".equals(url.getProtocol())) {
throw new IllegalArgumentException("URL does not reference a file");
}
try {
return new File(url.toURI());
}
catch (URISyntaxException ex) {
return new File(url.getPath());
}
private static boolean lineHasText(String line) {
return !line.trim().isEmpty();
}
}

@ -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();
}
}
}

@ -17,18 +17,41 @@
package org.springframework.boot.loader.launch;
/**
* Repackaged {@link org.springframework.boot.loader.JarLauncher}.
* {@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 3.2.0
*/
public final class JarLauncher {
public class JarLauncher extends ExecutableArchiveLauncher {
private JarLauncher() {
public JarLauncher() throws Exception {
}
protected JarLauncher(Archive archive) throws Exception {
super(archive);
}
@Override
protected boolean isIncludedOnClassPath(Archive.Entry entry) {
String name = entry.name();
if (entry.isDirectory()) {
return name.equals("BOOT-INF/classes/");
}
return name.startsWith("BOOT-INF/lib/");
}
@Override
protected String getEntryPathPrefix() {
return "BOOT-INF/";
}
public static void main(String[] args) throws Exception {
org.springframework.boot.loader.JarLauncher.main(args);
new JarLauncher().launch(args);
}
}

@ -14,27 +14,27 @@
* limitations under the License.
*/
package org.springframework.boot.loader.jarmode;
package org.springframework.boot.loader.launch;
import java.util.List;
import org.springframework.boot.loader.jarmode.JarMode;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.ClassUtils;
/**
* Delegate class used to launch the fat jar in a specific mode.
* Delegate class used to run the nested jar in a specific mode.
*
* @author Phillip Webb
* @since 2.3.0
*/
public final class JarModeLauncher {
final class JarModeRunner {
static final String DISABLE_SYSTEM_EXIT = JarModeLauncher.class.getName() + ".DISABLE_SYSTEM_EXIT";
static final String DISABLE_SYSTEM_EXIT = JarModeRunner.class.getName() + ".DISABLE_SYSTEM_EXIT";
private JarModeLauncher() {
private JarModeRunner() {
}
public static void main(String[] args) {
static void main(String[] args) {
String mode = System.getProperty("jarmode");
List<JarMode> candidates = SpringFactoriesLoader.loadFactories(JarMode.class,
ClassUtils.getDefaultClassLoader());

@ -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;
}

@ -16,19 +16,585 @@
package org.springframework.boot.loader.launch;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.Set;
import java.util.function.Predicate;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.boot.loader.launch.Archive.Entry;
import org.springframework.boot.loader.log.DebugLogger;
import org.springframework.boot.loader.net.protocol.jar.JarUrl;
/**
* Repackaged {@link org.springframework.boot.loader.PropertiesLauncher}.
* {@link Launcher} for archives with user-configured classpath and main class through a
* properties file.
* <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=my} will look for
* {@code my.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
* @author Phillip Webb
* @since 3.2.0
*/
public final class PropertiesLauncher {
public class PropertiesLauncher extends Launcher {
/**
* 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 URL[] NO_URLS = new URL[0];
private static final Pattern WORD_SEPARATOR = Pattern.compile("\\W+");
private static final String NESTED_ARCHIVE_SEPARATOR = "!" + File.separator;
private static final String JAR_FILE_PREFIX = "jar:file:";
private static final DebugLogger debug = DebugLogger.get(PropertiesLauncher.class);
private PropertiesLauncher() {
private final Archive archive;
private final File homeDirectory;
private final List<String> paths;
private final Properties properties = new Properties();
public PropertiesLauncher() throws Exception {
this.archive = Archive.create(Launcher.class);
this.homeDirectory = getHomeDirectory();
initializeProperties();
this.paths = getPaths();
}
protected File getHomeDirectory() throws Exception {
return new File(getPropertyWithDefault(HOME, "${user.dir}"));
}
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) {
String propertiesFile = name + ".properties";
configs.add("file:" + this.homeDirectory + "/" + propertiesFile);
configs.add("classpath:" + propertiesFile);
configs.add("classpath:BOOT-INF/classes/" + propertiesFile);
}
}
for (String config : configs) {
try (InputStream resource = getResource(config)) {
if (resource == null) {
debug.log("Not found: %s", config);
continue;
}
debug.log("Found: %s", config);
loadResource(resource);
return; // Load the first one we find
}
}
}
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 InputStream getClasspathResource(String config) {
config = stripLeadingSlashes(config);
config = "/" + config;
debug.log("Trying classpath: %s", config);
return getClass().getResourceAsStream(config);
}
private String handleUrl(String path) {
if (path.startsWith("jar:file:") || path.startsWith("file:")) {
path = URLDecoder.decode(path, StandardCharsets.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 getURLResource(String config) throws Exception {
URL url = new URL(config);
if (exists(url)) {
URLConnection connection = url.openConnection();
try {
return connection.getInputStream();
}
catch (IOException ex) {
disconnect(connection);
throw ex;
}
}
return null;
}
private boolean exists(URL url) throws IOException {
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;
}
if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
return false;
}
}
return (connection.getContentLength() >= 0);
}
finally {
disconnect(connection);
}
}
private void disconnect(URLConnection connection) {
if (connection instanceof HttpURLConnection httpConnection) {
httpConnection.disconnect();
}
}
private InputStream getFileResource(String config) throws Exception {
File file = new File(config);
debug.log("Trying file: %s", config);
return (!file.canRead()) ? null : new FileInputStream(file);
}
private void loadResource(InputStream resource) throws Exception {
this.properties.load(resource);
resolvePropertyPlaceholders();
if ("true".equalsIgnoreCase(getProperty(SET_SYSTEM_PROPERTIES))) {
addToSystemProperties();
}
}
private void resolvePropertyPlaceholders() {
for (String name : this.properties.stringPropertyNames()) {
String value = this.properties.getProperty(name);
String resolved = SystemPropertyUtils.resolvePlaceholders(this.properties, value);
if (resolved != null) {
this.properties.put(name, resolved);
}
}
}
private void addToSystemProperties() {
debug.log("Adding resolved properties to System properties");
for (String name : this.properties.stringPropertyNames()) {
String value = this.properties.getProperty(name);
System.setProperty(name, value);
}
}
private List<String> getPaths() throws Exception {
String path = getProperty(PATH);
List<String> paths = (path != null) ? parsePathsProperty(path) : Collections.emptyList();
debug.log("Nested archive paths: %s", this.paths);
return 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.isEmpty()) ? "/" : path;
paths.add(path);
}
if (paths.isEmpty()) {
paths.add("lib");
}
return paths;
}
private String cleanupPath(String path) {
path = path.trim();
// No need for current dir path
if (path.startsWith("./")) {
path = path.substring(2);
}
if (isArchive(path)) {
return path;
}
if (path.endsWith("/*")) {
return path.substring(0, path.length() - 1);
}
// It's a directory
return (!path.endsWith("/") && !path.equals(".")) ? path + "/" : path;
}
@Override
protected ClassLoader createClassLoader(Collection<URL> urls) throws Exception {
String loaderClassName = getProperty("loader.classLoader");
if (loaderClassName == null) {
return super.createClassLoader(urls);
}
ClassLoader parent = getClass().getClassLoader();
ClassLoader classLoader = new LaunchedClassLoader(false, urls.toArray(new URL[0]), parent);
debug.log("Classpath for custom loader: %s", urls);
classLoader = wrapWithCustomClassLoader(classLoader, loaderClassName);
debug.log("Using custom class loader: %s", loaderClassName);
return classLoader;
}
private ClassLoader wrapWithCustomClassLoader(ClassLoader parent, String loaderClassName) throws Exception {
Instantiator<ClassLoader> instantiator = new Instantiator<>(parent, loaderClassName);
ClassLoader loader = instantiator.declaredConstructor(ClassLoader.class).newInstance(parent);
loader = (loader != null) ? loader
: instantiator.declaredConstructor(URL[].class, ClassLoader.class).newInstance(NO_URLS, parent);
loader = (loader != null) ? loader : instantiator.constructWithoutParameters();
if (loader != null) {
return loader;
}
throw new IllegalStateException("Unable to create class loader for " + loaderClassName);
}
@Override
protected Archive getArchive() {
return null; // We don't have a single archive and are not exploded.
}
@Override
protected String getMainClass() throws Exception {
String mainClass = getProperty(MAIN, "Start-Class");
if (mainClass == null) {
throw new IllegalStateException("No '%s' or 'Start-Class' specified".formatted(MAIN));
}
return mainClass;
}
protected String[] getArgs(String... args) throws Exception {
String loaderArgs = getProperty(ARGS);
return (loaderArgs != null) ? merge(loaderArgs.split("\\s+"), args) : args;
}
private String[] merge(String[] a1, String[] a2) {
String[] result = new String[a1.length + a2.length];
System.arraycopy(a1, 0, result, 0, a1.length);
System.arraycopy(a2, 0, result, a1.length, a2.length);
return result;
}
private String getProperty(String name) throws Exception {
return getProperty(name, null, null);
}
private String getProperty(String name, String manifestKey) throws Exception {
return getProperty(name, manifestKey, null);
}
private String getPropertyWithDefault(String name, String defaultValue) throws Exception {
return getProperty(name, null, defaultValue);
}
private String getProperty(String name, String manifestKey, String defaultValue) throws Exception {
manifestKey = (manifestKey != null) ? manifestKey : toCamelCase(name.replace('.', '-'));
String value = SystemPropertyUtils.getProperty(name);
if (value != null) {
return getResolvedProperty(name, manifestKey, value, "environment");
}
if (this.properties.containsKey(name)) {
value = this.properties.getProperty(name);
return getResolvedProperty(name, manifestKey, value, "properties");
}
// Prefer home dir for MANIFEST if there is one
if (this.homeDirectory != null) {
try {
try (ExplodedArchive explodedArchive = new ExplodedArchive(this.homeDirectory)) {
value = getManifestValue(explodedArchive, manifestKey);
if (value != null) {
return getResolvedProperty(name, manifestKey, value, "home directory manifest");
}
}
}
catch (IllegalStateException ex) {
// Ignore
}
}
// Otherwise try the root archive
value = getManifestValue(this.archive, manifestKey);
if (value != null) {
return getResolvedProperty(name, manifestKey, value, "manifest");
}
return SystemPropertyUtils.resolvePlaceholders(this.properties, defaultValue);
}
String getManifestValue(Archive archive, String manifestKey) throws Exception {
Manifest manifest = archive.getManifest();
return (manifest != null) ? manifest.getMainAttributes().getValue(manifestKey) : null;
}
private String getResolvedProperty(String name, String manifestKey, String value, String from) {
value = SystemPropertyUtils.resolvePlaceholders(this.properties, value);
String altName = (manifestKey != null && !manifestKey.equals(name)) ? "[%s] ".formatted(manifestKey) : "";
debug.log("Property '%s'%s from %s: %s", name, altName, from, value);
return value;
}
void close() throws Exception {
if (this.archive != null) {
this.archive.close();
}
}
public static String toCamelCase(CharSequence string) {
if (string == null) {
return null;
}
StringBuilder result = new StringBuilder();
Matcher matcher = WORD_SEPARATOR.matcher(string);
int pos = 0;
while (matcher.find()) {
result.append(capitalize(string.subSequence(pos, matcher.end()).toString()));
pos = matcher.end();
}
result.append(capitalize(string.subSequence(pos, string.length()).toString()));
return result.toString();
}
private static String capitalize(String str) {
return Character.toUpperCase(str.charAt(0)) + str.substring(1);
}
@Override
protected Set<URL> getClassPathUrls() throws Exception {
Set<URL> urls = new LinkedHashSet<>();
for (String path : getPaths()) {
path = cleanupPath(handleUrl(path));
urls.addAll(getClassPathUrlsForPath(path));
}
return urls;
}
private Set<URL> getClassPathUrlsForPath(String path) throws Exception {
File file = (!isAbsolutePath(path)) ? new File(this.homeDirectory, path) : new File(path);
Set<URL> urls = new LinkedHashSet<>();
if (!"/".equals(path)) {
if (file.isDirectory()) {
try (ExplodedArchive explodedArchive = new ExplodedArchive(file)) {
debug.log("Adding classpath entries from directory %s", file);
urls.add(file.toURI().toURL());
urls.addAll(explodedArchive.getClassPathUrls(this::isArchive));
}
}
}
if (!file.getPath().contains(NESTED_ARCHIVE_SEPARATOR) && isArchive(file.getName())) {
debug.log("Adding classpath entries from jar/zip archive %s", path);
urls.add(file.toURI().toURL());
}
Set<URL> nested = getClassPathUrlsForNested(path);
if (!nested.isEmpty()) {
debug.log("Adding classpath entries from nested %s", path);
urls.addAll(nested);
}
return urls;
}
private Set<URL> getClassPathUrlsForNested(String path) throws Exception {
boolean isJustArchive = isArchive(path);
if (!path.equals("/") && path.startsWith("/")
|| (this.archive.isExploded() && this.archive.getRootDirectory().equals(this.homeDirectory))) {
return Collections.emptySet();
}
File file = null;
if (isJustArchive) {
File candidate = new File(this.homeDirectory, path);
if (candidate.exists()) {
file = candidate;
path = "";
}
}
int separatorIndex = path.indexOf('!');
if (separatorIndex != -1) {
file = (!path.startsWith(JAR_FILE_PREFIX)) ? new File(this.homeDirectory, path.substring(0, separatorIndex))
: new File(path.substring(JAR_FILE_PREFIX.length(), separatorIndex));
path = path.substring(separatorIndex + 1);
path = stripLeadingSlashes(path);
}
if (path.equals("/") || path.equals("./") || path.equals(".")) {
// The prefix for nested jars is actually empty if it's at the root
path = "";
}
Archive archive = (file != null) ? new JarFileArchive(file) : this.archive;
try {
Set<URL> urls = new LinkedHashSet<>(archive.getClassPathUrls(includeByPrefix(path)));
if (!isJustArchive && file != null && path.isEmpty()) {
urls.add(JarUrl.create(file));
}
return urls;
}
finally {
if (archive != this.archive) {
archive.close();
}
}
}
private Predicate<Entry> includeByPrefix(String prefix) {
return (entry) -> (entry.isDirectory() && entry.name().equals(prefix))
|| (isArchive(entry) && entry.name().startsWith(prefix));
}
private boolean isArchive(Entry entry) {
return isArchive(entry.name());
}
private boolean isArchive(String name) {
name = name.toLowerCase(Locale.ENGLISH);
return name.endsWith(".jar") || name.endsWith(".zip");
}
private boolean isAbsolutePath(String root) {
// Windows contains ":" others start with "/"
return root.contains(":") || root.startsWith("/");
}
private String stripLeadingSlashes(String string) {
while (string.startsWith("/")) {
string = string.substring(1);
}
return string;
}
public static void main(String[] args) throws Exception {
org.springframework.boot.loader.PropertiesLauncher.main(args);
PropertiesLauncher launcher = new PropertiesLauncher();
args = launcher.getArgs(args);
launcher.launch(args);
}
/**
* Utility to help instantiate objects.
*/
private record Instantiator<T>(ClassLoader parent, Class<?> type) {
Instantiator(ClassLoader parent, String className) throws ClassNotFoundException {
this(parent, Class.forName(className, true, parent));
}
T constructWithoutParameters() throws Exception {
return declaredConstructor().newInstance();
}
Using<T> declaredConstructor(Class<?>... parameterTypes) {
return new Using<>(this, parameterTypes);
}
private record Using<T>(Instantiator<T> instantiator, Class<?>... parameterTypes) {
@SuppressWarnings("unchecked")
T newInstance(Object... initargs) throws Exception {
try {
Constructor<?> constructor = this.instantiator.type().getDeclaredConstructor(this.parameterTypes);
constructor.setAccessible(true);
return (T) constructor.newInstance(initargs);
}
catch (NoSuchMethodException ex) {
return null;
}
}
}
}
}

@ -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;
}
}

@ -17,18 +17,40 @@
package org.springframework.boot.loader.launch;
/**
* Repackaged {@link org.springframework.boot.loader.WarLauncher}.
* {@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 3.2.0
*/
public final class WarLauncher {
public class WarLauncher extends ExecutableArchiveLauncher {
private WarLauncher() {
public WarLauncher() throws Exception {
}
protected WarLauncher(Archive archive) throws Exception {
super(archive);
}
@Override
public boolean isIncludedOnClassPath(Archive.Entry entry) {
String name = entry.name();
if (entry.isDirectory()) {
return name.equals("WEB-INF/classes/");
}
return name.startsWith("WEB-INF/lib/") || name.startsWith("WEB-INF/lib-provided/");
}
@Override
protected String getEntryPathPrefix() {
return "WEB-INF/";
}
public static void main(String[] args) throws Exception {
org.springframework.boot.loader.WarLauncher.main(args);
new WarLauncher().launch(args);
}
}

@ -15,7 +15,10 @@
*/
/**
* Repackaged launcher classes.
* 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.launch.JarLauncher
* @see org.springframework.boot.loader.launch.WarLauncher

@ -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);
}
}
}

@ -15,8 +15,6 @@
*/
/**
* Classes and interfaces to allow random access to a block of data.
*
* @see org.springframework.boot.loader.data.RandomAccessData
* Debug {@link java.lang.System#err} logging support.
*/
package org.springframework.boot.loader.data;
package org.springframework.boot.loader.log;

@ -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;
}

@ -14,22 +14,34 @@
* limitations under the License.
*/
package org.springframework.boot.loader.jar;
package org.springframework.boot.loader.net.protocol.jar;
/**
* Interface that can be used to filter and optionally rename jar entries.
* {@link ThreadLocal} state for {@link Handler} optimizations.
*
* @author Phillip Webb
*/
interface JarEntryFilter {
/**
* Apply the jar entry filter.
* @param name the current entry name. This may be different that the original entry
* name if a previous filter has been applied
* @return the new name of the entry or {@code null} if the entry should not be
* included.
*/
AsciiBytes apply(AsciiBytes name);
final class Optimizations {
private static final ThreadLocal<Boolean> status = new ThreadLocal<>();
private Optimizations() {
}
static void enable(boolean readContents) {
status.set(readContents);
}
static void disable() {
status.remove();
}
static boolean isEnabled() {
return status.get() != null;
}
static boolean isEnabled(boolean readContents) {
return Boolean.valueOf(readContents).equals(status.get());
}
}

@ -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:&lt;path&gt;/!{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;

@ -15,9 +15,6 @@
*/
/**
* Abstraction over logical Archives be they backed by a JAR file or unpacked into a
* directory.
*
* @see org.springframework.boot.loader.archive.Archive
* {@link java.net.URL} protocol support.
*/
package org.springframework.boot.loader.archive;
package org.springframework.boot.loader.net.protocol;

@ -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");
}
}
}

@ -15,6 +15,6 @@
*/
/**
* Utilities used by Spring Boot's JAR loading.
* Net utilities.
*/
package org.springframework.boot.loader.util;
package org.springframework.boot.loader.net.util;

@ -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;
}
}

@ -14,24 +14,16 @@
* limitations under the License.
*/
package org.springframework.boot.loader.jar;
package org.springframework.boot.loader.zip;
import java.io.Closeable;
/**
* Utilities for dealing with bytes from ZIP files.
* A {@link Closeable} {@link DataBlock}.
*
* @author Phillip Webb
* @since 3.2.0
*/
final class Bytes {
private Bytes() {
}
static long littleEndianValue(byte[] bytes, int offset, int length) {
long value = 0;
for (int i = length - 1; i >= 0; i--) {
value = ((value << 8) | (bytes[offset + i] & 0xFF));
}
return value;
}
public interface CloseableDataBlock extends DataBlock, Closeable {
}

@ -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…
Cancel
Save