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
This commit is contained in:
Phillip Webb
2023-09-18 12:30:13 -07:00
parent 75ddb9fa47
commit 7ad4a9817d
161 changed files with 13905 additions and 9577 deletions

View File

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

View File

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

View File

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

View File

@@ -1,300 +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.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
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 static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
/**
* Tests for {@link RandomAccessDataFile}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class RandomAccessDataFileTests {
private static final byte[] BYTES;
static {
BYTES = new byte[256];
for (int i = 0; i < BYTES.length; i++) {
BYTES[i] = (byte) i;
}
}
private File tempFile;
private RandomAccessDataFile file;
private InputStream inputStream;
@BeforeEach
void setup(@TempDir File tempDir) throws Exception {
this.tempFile = new File(tempDir, "tempFile");
FileOutputStream outputStream = new FileOutputStream(this.tempFile);
outputStream.write(BYTES);
outputStream.close();
this.file = new RandomAccessDataFile(this.tempFile);
this.inputStream = this.file.getInputStream();
}
@AfterEach
void cleanup() throws Exception {
this.inputStream.close();
this.file.close();
}
@Test
void fileNotNull() {
assertThatIllegalArgumentException().isThrownBy(() -> new RandomAccessDataFile(null))
.withMessageContaining("File must not be null");
}
@Test
void fileExists() {
File file = new File("/does/not/exist");
assertThatIllegalArgumentException().isThrownBy(() -> new RandomAccessDataFile(file))
.withMessageContaining(String.format("File %s must exist", file.getAbsolutePath()));
}
@Test
void readWithOffsetAndLengthShouldRead() throws Exception {
byte[] read = this.file.read(2, 3);
assertThat(read).isEqualTo(new byte[] { 2, 3, 4 });
}
@Test
void readWhenOffsetIsBeyondEOFShouldThrowException() {
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.read(257, 0));
}
@Test
void readWhenOffsetIsBeyondEndOfSubsectionShouldThrowException() {
RandomAccessData subsection = this.file.getSubsection(0, 10);
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> subsection.read(11, 0));
}
@Test
void readWhenOffsetPlusLengthGreaterThanEOFShouldThrowException() {
assertThatExceptionOfType(EOFException.class).isThrownBy(() -> this.file.read(256, 1));
}
@Test
void readWhenOffsetPlusLengthGreaterThanEndOfSubsectionShouldThrowException() {
RandomAccessData subsection = this.file.getSubsection(0, 10);
assertThatExceptionOfType(EOFException.class).isThrownBy(() -> subsection.read(10, 1));
}
@Test
void inputStreamRead() throws Exception {
for (int i = 0; i <= 255; i++) {
assertThat(this.inputStream.read()).isEqualTo(i);
}
}
@Test
void inputStreamReadNullBytes() {
assertThatNullPointerException().isThrownBy(() -> this.inputStream.read(null))
.withMessage("Bytes must not be null");
}
@Test
void inputStreamReadNullBytesWithOffset() {
assertThatNullPointerException().isThrownBy(() -> this.inputStream.read(null, 0, 1))
.withMessage("Bytes must not be null");
}
@Test
void inputStreamReadBytes() throws Exception {
byte[] b = new byte[256];
int amountRead = this.inputStream.read(b);
assertThat(b).isEqualTo(BYTES);
assertThat(amountRead).isEqualTo(256);
}
@Test
void inputStreamReadOffsetBytes() throws Exception {
byte[] b = new byte[7];
this.inputStream.skip(1);
int amountRead = this.inputStream.read(b, 2, 3);
assertThat(b).isEqualTo(new byte[] { 0, 0, 1, 2, 3, 0, 0 });
assertThat(amountRead).isEqualTo(3);
}
@Test
void inputStreamReadMoreBytesThanAvailable() throws Exception {
byte[] b = new byte[257];
int amountRead = this.inputStream.read(b);
assertThat(b).startsWith(BYTES);
assertThat(amountRead).isEqualTo(256);
}
@Test
void inputStreamReadPastEnd() throws Exception {
this.inputStream.skip(255);
assertThat(this.inputStream.read()).isEqualTo(0xFF);
assertThat(this.inputStream.read()).isEqualTo(-1);
assertThat(this.inputStream.read()).isEqualTo(-1);
}
@Test
void inputStreamReadZeroLength() throws Exception {
byte[] b = new byte[] { 0x0F };
int amountRead = this.inputStream.read(b, 0, 0);
assertThat(b).isEqualTo(new byte[] { 0x0F });
assertThat(amountRead).isZero();
assertThat(this.inputStream.read()).isZero();
}
@Test
void inputStreamSkip() throws Exception {
long amountSkipped = this.inputStream.skip(4);
assertThat(this.inputStream.read()).isEqualTo(4);
assertThat(amountSkipped).isEqualTo(4L);
}
@Test
void inputStreamSkipMoreThanAvailable() throws Exception {
long amountSkipped = this.inputStream.skip(257);
assertThat(this.inputStream.read()).isEqualTo(-1);
assertThat(amountSkipped).isEqualTo(256L);
}
@Test
void inputStreamSkipPastEnd() throws Exception {
this.inputStream.skip(256);
long amountSkipped = this.inputStream.skip(1);
assertThat(amountSkipped).isZero();
}
@Test
void inputStreamAvailable() throws Exception {
assertThat(this.inputStream.available()).isEqualTo(256);
this.inputStream.skip(56);
assertThat(this.inputStream.available()).isEqualTo(200);
this.inputStream.skip(200);
assertThat(this.inputStream.available()).isZero();
}
@Test
void subsectionNegativeOffset() {
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(-1, 1));
}
@Test
void subsectionNegativeLength() {
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(0, -1));
}
@Test
void subsectionZeroLength() throws Exception {
RandomAccessData subsection = this.file.getSubsection(0, 0);
assertThat(subsection.getInputStream().read()).isEqualTo(-1);
}
@Test
void subsectionTooBig() {
this.file.getSubsection(0, 256);
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(0, 257));
}
@Test
void subsectionTooBigWithOffset() {
this.file.getSubsection(1, 255);
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(1, 256));
}
@Test
void subsection() throws Exception {
RandomAccessData subsection = this.file.getSubsection(1, 1);
assertThat(subsection.getInputStream().read()).isOne();
}
@Test
void inputStreamReadPastSubsection() throws Exception {
RandomAccessData subsection = this.file.getSubsection(1, 2);
InputStream inputStream = subsection.getInputStream();
assertThat(inputStream.read()).isOne();
assertThat(inputStream.read()).isEqualTo(2);
assertThat(inputStream.read()).isEqualTo(-1);
}
@Test
void inputStreamReadBytesPastSubsection() throws Exception {
RandomAccessData subsection = this.file.getSubsection(1, 2);
InputStream inputStream = subsection.getInputStream();
byte[] b = new byte[3];
int amountRead = inputStream.read(b);
assertThat(b).isEqualTo(new byte[] { 1, 2, 0 });
assertThat(amountRead).isEqualTo(2);
}
@Test
void inputStreamSkipPastSubsection() throws Exception {
RandomAccessData subsection = this.file.getSubsection(1, 2);
InputStream inputStream = subsection.getInputStream();
assertThat(inputStream.skip(3)).isEqualTo(2L);
assertThat(inputStream.read()).isEqualTo(-1);
}
@Test
void inputStreamSkipNegative() throws Exception {
assertThat(this.inputStream.skip(-1)).isZero();
}
@Test
void getFile() {
assertThat(this.file.getFile()).isEqualTo(this.tempFile);
}
@Test
void concurrentReads() throws Exception {
ExecutorService executorService = Executors.newFixedThreadPool(20);
List<Future<Boolean>> results = new ArrayList<>();
for (int i = 0; i < 100; i++) {
results.add(executorService.submit(() -> {
InputStream subsectionInputStream = RandomAccessDataFileTests.this.file.getSubsection(0, 256)
.getInputStream();
byte[] b = new byte[256];
subsectionInputStream.read(b);
return Arrays.equals(b, BYTES);
}));
}
for (Future<Boolean> future : results) {
assertThat(future.get()).isTrue();
}
}
}

View File

@@ -1,196 +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 org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link AsciiBytes}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class AsciiBytesTests {
private static final char NO_SUFFIX = 0;
@Test
void createFromBytes() {
AsciiBytes bytes = new AsciiBytes(new byte[] { 65, 66 });
assertThat(bytes).hasToString("AB");
}
@Test
void createFromBytesWithOffset() {
AsciiBytes bytes = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
assertThat(bytes).hasToString("BC");
}
@Test
void createFromString() {
AsciiBytes bytes = new AsciiBytes("AB");
assertThat(bytes).hasToString("AB");
}
@Test
void length() {
AsciiBytes b1 = new AsciiBytes(new byte[] { 65, 66 });
AsciiBytes b2 = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
assertThat(b1.length()).isEqualTo(2);
assertThat(b2.length()).isEqualTo(2);
}
@Test
void startWith() {
AsciiBytes abc = new AsciiBytes(new byte[] { 65, 66, 67 });
AsciiBytes ab = new AsciiBytes(new byte[] { 65, 66 });
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67 }, 1, 2);
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
assertThat(abc.startsWith(abc)).isTrue();
assertThat(abc.startsWith(ab)).isTrue();
assertThat(abc.startsWith(bc)).isFalse();
assertThat(abc.startsWith(abcd)).isFalse();
}
@Test
void endsWith() {
AsciiBytes abc = new AsciiBytes(new byte[] { 65, 66, 67 });
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67 }, 1, 2);
AsciiBytes ab = new AsciiBytes(new byte[] { 65, 66 });
AsciiBytes aabc = new AsciiBytes(new byte[] { 65, 65, 66, 67 });
assertThat(abc.endsWith(abc)).isTrue();
assertThat(abc.endsWith(bc)).isTrue();
assertThat(abc.endsWith(ab)).isFalse();
assertThat(abc.endsWith(aabc)).isFalse();
}
@Test
void substringFromBeingIndex() {
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
assertThat(abcd.substring(0)).hasToString("ABCD");
assertThat(abcd.substring(1)).hasToString("BCD");
assertThat(abcd.substring(2)).hasToString("CD");
assertThat(abcd.substring(3)).hasToString("D");
assertThat(abcd.substring(4).toString()).isEmpty();
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> abcd.substring(5));
}
@Test
void substring() {
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
assertThat(abcd.substring(0, 4)).hasToString("ABCD");
assertThat(abcd.substring(1, 3)).hasToString("BC");
assertThat(abcd.substring(3, 4)).hasToString("D");
assertThat(abcd.substring(3, 3).toString()).isEmpty();
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> abcd.substring(3, 5));
}
@Test
void hashCodeAndEquals() {
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
AsciiBytes bc = new AsciiBytes(new byte[] { 66, 67 });
AsciiBytes bc_substring = new AsciiBytes(new byte[] { 65, 66, 67, 68 }).substring(1, 3);
AsciiBytes bc_string = new AsciiBytes("BC");
assertThat(bc).hasSameHashCodeAs(bc);
assertThat(bc).hasSameHashCodeAs(bc_substring);
assertThat(bc).hasSameHashCodeAs(bc_string);
assertThat(bc).isEqualTo(bc);
assertThat(bc).isEqualTo(bc_substring);
assertThat(bc).isEqualTo(bc_string);
assertThat(bc.hashCode()).isNotEqualTo(abcd.hashCode());
assertThat(bc).isNotEqualTo(abcd);
}
@Test
void hashCodeSameAsString() {
hashCodeSameAsString("abcABC123xyz!");
}
@Test
void hashCodeSameAsStringWithSpecial() {
hashCodeSameAsString("special/\u00EB.dat");
}
@Test
void hashCodeSameAsStringWithCyrillicCharacters() {
hashCodeSameAsString("\u0432\u0435\u0441\u043D\u0430");
}
@Test
void hashCodeSameAsStringWithEmoji() {
hashCodeSameAsString("\ud83d\udca9");
}
private void hashCodeSameAsString(String input) {
assertThat(new AsciiBytes(input)).hasSameHashCodeAs(input);
}
@Test
void matchesSameAsString() {
matchesSameAsString("abcABC123xyz!");
}
@Test
void matchesSameAsStringWithSpecial() {
matchesSameAsString("special/\u00EB.dat");
}
@Test
void matchesSameAsStringWithCyrillicCharacters() {
matchesSameAsString("\u0432\u0435\u0441\u043D\u0430");
}
@Test
void matchesDifferentLengths() {
assertThat(new AsciiBytes("abc").matches("ab", NO_SUFFIX)).isFalse();
assertThat(new AsciiBytes("abc").matches("abcd", NO_SUFFIX)).isFalse();
assertThat(new AsciiBytes("abc").matches("abc", NO_SUFFIX)).isTrue();
assertThat(new AsciiBytes("abc").matches("a", 'b')).isFalse();
assertThat(new AsciiBytes("abc").matches("abc", 'd')).isFalse();
assertThat(new AsciiBytes("abc").matches("ab", 'c')).isTrue();
}
@Test
void matchesSuffix() {
assertThat(new AsciiBytes("ab").matches("a", 'b')).isTrue();
}
@Test
void matchesSameAsStringWithEmoji() {
matchesSameAsString("\ud83d\udca9");
}
@Test
void hashCodeFromInstanceMatchesHashCodeFromString() {
String name = "fonts/宋体/simsun.ttf";
assertThat(new AsciiBytes(name).hashCode()).isEqualTo(AsciiBytes.hashCode(name));
}
@Test
void instanceCreatedFromCharSequenceMatchesSameCharSequence() {
String name = "fonts/宋体/simsun.ttf";
assertThat(new AsciiBytes(name).matches(name, NO_SUFFIX)).isTrue();
}
private void matchesSameAsString(String input) {
assertThat(new AsciiBytes(input).matches(input, NO_SUFFIX)).isTrue();
}
}

View File

@@ -1,139 +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.util.ArrayList;
import java.util.Iterator;
import java.util.List;
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.data.RandomAccessData;
import org.springframework.boot.loader.data.RandomAccessDataFile;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CentralDirectoryParser}.
*
* @author Phillip Webb
*/
class CentralDirectoryParserTests {
private File jarFile;
private RandomAccessDataFile jarData;
@BeforeEach
void setup(@TempDir File tempDir) throws Exception {
this.jarFile = new File(tempDir, "test.jar");
TestJarCreator.createTestJar(this.jarFile);
this.jarData = new RandomAccessDataFile(this.jarFile);
}
@AfterEach
void tearDown() throws IOException {
this.jarData.close();
}
@Test
void visitsInOrder() throws Exception {
MockCentralDirectoryVisitor visitor = new MockCentralDirectoryVisitor();
CentralDirectoryParser parser = new CentralDirectoryParser();
parser.addVisitor(visitor);
parser.parse(this.jarData, false);
List<String> invocations = visitor.getInvocations();
assertThat(invocations).startsWith("visitStart").endsWith("visitEnd").contains("visitFileHeader");
}
@Test
void visitRecords() throws Exception {
Collector collector = new Collector();
CentralDirectoryParser parser = new CentralDirectoryParser();
parser.addVisitor(collector);
parser.parse(this.jarData, false);
Iterator<CentralDirectoryFileHeader> headers = collector.getHeaders().iterator();
assertThat(headers.next().getName()).hasToString("META-INF/");
assertThat(headers.next().getName()).hasToString("META-INF/MANIFEST.MF");
assertThat(headers.next().getName()).hasToString("1.dat");
assertThat(headers.next().getName()).hasToString("2.dat");
assertThat(headers.next().getName()).hasToString("d/");
assertThat(headers.next().getName()).hasToString("d/9.dat");
assertThat(headers.next().getName()).hasToString("special/");
assertThat(headers.next().getName()).hasToString("special/\u00EB.dat");
assertThat(headers.next().getName()).hasToString("nested.jar");
assertThat(headers.next().getName()).hasToString("another-nested.jar");
assertThat(headers.next().getName()).hasToString("space nested.jar");
assertThat(headers.next().getName()).hasToString("multi-release.jar");
assertThat(headers.hasNext()).isFalse();
}
static class Collector implements CentralDirectoryVisitor {
private final List<CentralDirectoryFileHeader> headers = new ArrayList<>();
@Override
public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) {
}
@Override
public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) {
this.headers.add(fileHeader.clone());
}
@Override
public void visitEnd() {
}
List<CentralDirectoryFileHeader> getHeaders() {
return this.headers;
}
}
static class MockCentralDirectoryVisitor implements CentralDirectoryVisitor {
private final List<String> invocations = new ArrayList<>();
@Override
public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) {
this.invocations.add("visitStart");
}
@Override
public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) {
this.invocations.add("visitFileHeader");
}
@Override
public void visitEnd() {
this.invocations.add("visitEnd");
}
List<String> getInvocations() {
return this.invocations;
}
}
}

View File

@@ -1,210 +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.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.loader.TestJarCreator;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Handler}.
*
* @author Andy Wilkinson
*/
@ExtendWith(JarUrlProtocolHandler.class)
class HandlerTests {
private final Handler handler = new Handler();
@Test
void parseUrlWithJarRootContextAndAbsoluteSpecThatUsesContext() throws MalformedURLException {
String spec = "/entry.txt";
URL context = createUrl("file:example.jar!/");
this.handler.parseURL(context, spec, 0, spec.length());
assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt");
}
@Test
void parseUrlWithDirectoryEntryContextAndAbsoluteSpecThatUsesContext() throws MalformedURLException {
String spec = "/entry.txt";
URL context = createUrl("file:example.jar!/dir/");
this.handler.parseURL(context, spec, 0, spec.length());
assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt");
}
@Test
void parseUrlWithJarRootContextAndRelativeSpecThatUsesContext() throws MalformedURLException {
String spec = "entry.txt";
URL context = createUrl("file:example.jar!/");
this.handler.parseURL(context, spec, 0, spec.length());
assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt");
}
@Test
void parseUrlWithDirectoryEntryContextAndRelativeSpecThatUsesContext() throws MalformedURLException {
String spec = "entry.txt";
URL context = createUrl("file:example.jar!/dir/");
this.handler.parseURL(context, spec, 0, spec.length());
assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/dir/entry.txt");
}
@Test
void parseUrlWithFileEntryContextAndRelativeSpecThatUsesContext() throws MalformedURLException {
String spec = "entry.txt";
URL context = createUrl("file:example.jar!/dir/file");
this.handler.parseURL(context, spec, 0, spec.length());
assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/dir/entry.txt");
}
@Test
void parseUrlWithSpecThatIgnoresContext() throws MalformedURLException {
JarFile.registerUrlProtocolHandler();
String spec = "jar:file:/other.jar!/nested!/entry.txt";
URL context = createUrl("file:example.jar!/dir/file");
this.handler.parseURL(context, spec, 0, spec.length());
assertThat(context.toExternalForm()).isEqualTo("jar:jar:file:/other.jar!/nested!/entry.txt");
}
@Test
void sameFileReturnsFalseForUrlsWithDifferentProtocols() throws MalformedURLException {
assertThat(this.handler.sameFile(new URL("jar:file:foo.jar!/content.txt"), new URL("file:/foo.jar"))).isFalse();
}
@Test
void sameFileReturnsFalseForDifferentFileInSameJar() throws MalformedURLException {
assertThat(this.handler.sameFile(new URL("jar:file:foo.jar!/the/path/to/the/first/content.txt"),
new URL("jar:file:/foo.jar!/content.txt")))
.isFalse();
}
@Test
void sameFileReturnsFalseForSameFileInDifferentJars() throws MalformedURLException {
assertThat(this.handler.sameFile(new URL("jar:file:/the/path/to/the/first.jar!/content.txt"),
new URL("jar:file:/second.jar!/content.txt")))
.isFalse();
}
@Test
void sameFileReturnsTrueForSameFileInSameJar() throws MalformedURLException {
assertThat(this.handler.sameFile(new URL("jar:file:/the/path/to/the/first.jar!/content.txt"),
new URL("jar:file:/the/path/to/the/first.jar!/content.txt")))
.isTrue();
}
@Test
void sameFileReturnsTrueForUrlsThatReferenceSameFileViaNestedArchiveAndFromRootOfJar()
throws MalformedURLException {
assertThat(this.handler.sameFile(new URL("jar:file:/test.jar!/BOOT-INF/classes!/foo.txt"),
new URL("jar:file:/test.jar!/BOOT-INF/classes/foo.txt")))
.isTrue();
}
@Test
void hashCodesAreEqualForUrlsThatReferenceSameFileViaNestedArchiveAndFromRootOfJar() throws MalformedURLException {
assertThat(this.handler.hashCode(new URL("jar:file:/test.jar!/BOOT-INF/classes!/foo.txt")))
.isEqualTo(this.handler.hashCode(new URL("jar:file:/test.jar!/BOOT-INF/classes/foo.txt")));
}
@Test
void urlWithSpecReferencingParentDirectory() throws MalformedURLException {
assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/directoryA/a.xsd",
"../directoryB/c/d/e.xsd");
}
@Test
void urlWithSpecReferencingAncestorDirectoryOutsideJarStopsAtJarRoot() throws MalformedURLException {
assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/directoryA/a.xsd",
"../../../../../../directoryB/b.xsd");
}
@Test
void urlWithSpecReferencingCurrentDirectory() throws MalformedURLException {
assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/directoryA/a.xsd",
"./directoryB/c/d/e.xsd");
}
@Test
void urlWithRef() throws MalformedURLException {
assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes", "!/foo.txt#alpha");
}
@Test
void urlWithQuery() throws MalformedURLException {
assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes", "!/foo.txt?alpha");
}
@Test
void fallbackToJdksJarUrlStreamHandler(@TempDir File tempDir) throws Exception {
File testJar = new File(tempDir, "test.jar");
TestJarCreator.createTestJar(testJar);
URLConnection connection = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/", this.handler)
.openConnection();
assertThat(connection).isInstanceOf(JarURLConnection.class);
((JarURLConnection) connection).getJarFile().close();
URLConnection jdkConnection = new URL(null, "jar:file:" + testJar.toURI().toURL() + "!/nested.jar!/",
this.handler)
.openConnection();
assertThat(jdkConnection).isNotInstanceOf(JarURLConnection.class);
assertThat(jdkConnection.getClass().getName()).endsWith(".JarURLConnection");
}
@Test
void whenJarHasAPlusInItsPathConnectionJarFileMatchesOriginalJarFile(@TempDir File tempDir) throws Exception {
File testJar = new File(tempDir, "t+e+s+t.jar");
TestJarCreator.createTestJar(testJar);
URL url = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/3.dat", this.handler);
JarURLConnection connection = (JarURLConnection) url.openConnection();
try (JarFile jarFile = JarFileWrapper.unwrap(connection.getJarFile())) {
assertThat(jarFile.getRootJarFile().getFile()).isEqualTo(testJar);
}
}
@Test
void whenJarHasASpaceInItsPathConnectionJarFileMatchesOriginalJarFile(@TempDir File tempDir) throws Exception {
File testJar = new File(tempDir, "t e s t.jar");
TestJarCreator.createTestJar(testJar);
URL url = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/3.dat", this.handler);
JarURLConnection connection = (JarURLConnection) url.openConnection();
try (JarFile jarFile = JarFileWrapper.unwrap(connection.getJarFile())) {
assertThat(jarFile.getRootJarFile().getFile()).isEqualTo(testJar);
}
}
private void assertStandardAndCustomHandlerUrlsAreEqual(String context, String spec) throws MalformedURLException {
URL standardUrl = new URL(new URL("jar:" + context), spec);
URL customHandlerUrl = new URL(new URL("jar", null, -1, context, this.handler), spec);
assertThat(customHandlerUrl).hasToString(standardUrl.toString());
assertThat(customHandlerUrl.getFile()).isEqualTo(standardUrl.getFile());
assertThat(customHandlerUrl.getPath()).isEqualTo(standardUrl.getPath());
assertThat(customHandlerUrl.getQuery()).isEqualTo(standardUrl.getQuery());
assertThat(customHandlerUrl.getRef()).isEqualTo(standardUrl.getRef());
}
private URL createUrl(String file) throws MalformedURLException {
return new URL("jar", null, -1, file, this.handler);
}
}

View File

@@ -1,736 +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.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilePermission;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.stream.Stream;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.loader.TestJarCreator;
import org.springframework.boot.loader.data.RandomAccessDataFile;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StopWatch;
import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIOException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.spy;
/**
* Tests for {@link JarFile}.
*
* @author Phillip Webb
* @author Martin Lau
* @author Andy Wilkinson
* @author Madhura Bhave
*/
@ExtendWith(JarUrlProtocolHandler.class)
class JarFileTests {
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
@TempDir
File tempDir;
private File rootJarFile;
private JarFile jarFile;
@BeforeEach
void setup() throws Exception {
this.rootJarFile = new File(this.tempDir, "root.jar");
TestJarCreator.createTestJar(this.rootJarFile);
this.jarFile = new JarFile(this.rootJarFile);
}
@AfterEach
void tearDown() throws Exception {
this.jarFile.close();
}
@Test
void jdkJarFile() throws Exception {
// Sanity checks to see how the default jar file operates
java.util.jar.JarFile jarFile = new java.util.jar.JarFile(this.rootJarFile);
assertThat(jarFile.getComment()).isEqualTo("outer");
Enumeration<java.util.jar.JarEntry> entries = jarFile.entries();
assertThat(entries.nextElement().getName()).isEqualTo("META-INF/");
assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF");
assertThat(entries.nextElement().getName()).isEqualTo("1.dat");
assertThat(entries.nextElement().getName()).isEqualTo("2.dat");
assertThat(entries.nextElement().getName()).isEqualTo("d/");
assertThat(entries.nextElement().getName()).isEqualTo("d/9.dat");
assertThat(entries.nextElement().getName()).isEqualTo("special/");
assertThat(entries.nextElement().getName()).isEqualTo("special/\u00EB.dat");
assertThat(entries.nextElement().getName()).isEqualTo("nested.jar");
assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar");
assertThat(entries.nextElement().getName()).isEqualTo("space nested.jar");
assertThat(entries.nextElement().getName()).isEqualTo("multi-release.jar");
assertThat(entries.hasMoreElements()).isFalse();
URL jarUrl = new URL("jar:" + this.rootJarFile.toURI() + "!/");
URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { jarUrl });
assertThat(urlClassLoader.getResource("special/\u00EB.dat")).isNotNull();
assertThat(urlClassLoader.getResource("d/9.dat")).isNotNull();
urlClassLoader.close();
jarFile.close();
}
@Test
void createFromFile() throws Exception {
JarFile jarFile = new JarFile(this.rootJarFile);
assertThat(jarFile.getName()).isNotNull();
jarFile.close();
}
@Test
void getManifest() throws Exception {
assertThat(this.jarFile.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1");
}
@Test
void getManifestEntry() throws Exception {
ZipEntry entry = this.jarFile.getJarEntry("META-INF/MANIFEST.MF");
Manifest manifest = new Manifest(this.jarFile.getInputStream(entry));
assertThat(manifest.getMainAttributes().getValue("Built-By")).isEqualTo("j1");
}
@Test
void getEntries() {
Enumeration<java.util.jar.JarEntry> entries = this.jarFile.entries();
assertThat(entries.nextElement().getName()).isEqualTo("META-INF/");
assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF");
assertThat(entries.nextElement().getName()).isEqualTo("1.dat");
assertThat(entries.nextElement().getName()).isEqualTo("2.dat");
assertThat(entries.nextElement().getName()).isEqualTo("d/");
assertThat(entries.nextElement().getName()).isEqualTo("d/9.dat");
assertThat(entries.nextElement().getName()).isEqualTo("special/");
assertThat(entries.nextElement().getName()).isEqualTo("special/\u00EB.dat");
assertThat(entries.nextElement().getName()).isEqualTo("nested.jar");
assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar");
assertThat(entries.nextElement().getName()).isEqualTo("space nested.jar");
assertThat(entries.nextElement().getName()).isEqualTo("multi-release.jar");
assertThat(entries.hasMoreElements()).isFalse();
}
@Test
void getSpecialResourceViaClassLoader() throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { this.jarFile.getUrl() });
assertThat(urlClassLoader.getResource("special/\u00EB.dat")).isNotNull();
urlClassLoader.close();
}
@Test
void getJarEntry() {
java.util.jar.JarEntry entry = this.jarFile.getJarEntry("1.dat");
assertThat(entry).isNotNull();
assertThat(entry.getName()).isEqualTo("1.dat");
}
@Test
void getJarEntryWhenClosed() throws Exception {
this.jarFile.close();
assertThatZipFileClosedIsThrownBy(() -> this.jarFile.getJarEntry("1.dat"));
}
@Test
void getInputStream() throws Exception {
InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("1.dat"));
assertThat(inputStream.available()).isOne();
assertThat(inputStream.read()).isOne();
assertThat(inputStream.available()).isZero();
assertThat(inputStream.read()).isEqualTo(-1);
}
@Test
void getInputStreamWhenClosed() throws Exception {
ZipEntry entry = this.jarFile.getEntry("1.dat");
this.jarFile.close();
assertThatZipFileClosedIsThrownBy(() -> this.jarFile.getInputStream(entry));
}
@Test
void getComment() {
assertThat(this.jarFile.getComment()).isEqualTo("outer");
}
@Test
void getCommentWhenClosed() throws Exception {
this.jarFile.close();
assertThatZipFileClosedIsThrownBy(() -> this.jarFile.getComment());
}
@Test
void getName() {
assertThat(this.jarFile.getName()).isEqualTo(this.rootJarFile.getPath());
}
@Test
void size() throws Exception {
try (ZipFile zip = new ZipFile(this.rootJarFile)) {
assertThat(this.jarFile).hasSize(zip.size());
}
}
@Test
void sizeWhenClosed() throws Exception {
this.jarFile.close();
assertThatZipFileClosedIsThrownBy(() -> this.jarFile.size());
}
@Test
void getEntryTime() throws Exception {
java.util.jar.JarFile jdkJarFile = new java.util.jar.JarFile(this.rootJarFile);
assertThat(this.jarFile.getEntry("META-INF/MANIFEST.MF").getTime())
.isEqualTo(jdkJarFile.getEntry("META-INF/MANIFEST.MF").getTime());
jdkJarFile.close();
}
@Test
void close() throws Exception {
RandomAccessDataFile randomAccessDataFile = spy(new RandomAccessDataFile(this.rootJarFile));
JarFile jarFile = new JarFile(randomAccessDataFile);
jarFile.close();
then(randomAccessDataFile).should().close();
}
@Test
void getUrl() throws Exception {
URL url = this.jarFile.getUrl();
assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/");
JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
assertThat(JarFileWrapper.unwrap(jarURLConnection.getJarFile())).isSameAs(this.jarFile);
assertThat(jarURLConnection.getJarEntry()).isNull();
assertThat(jarURLConnection.getContentLength()).isGreaterThan(1);
assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) jarURLConnection.getContent())).isSameAs(this.jarFile);
assertThat(jarURLConnection.getContentType()).isEqualTo("x-java/jar");
assertThat(jarURLConnection.getJarFileURL().toURI()).isEqualTo(this.rootJarFile.toURI());
}
@Test
void createEntryUrl() throws Exception {
URL url = new URL(this.jarFile.getUrl(), "1.dat");
assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/1.dat");
JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
assertThat(JarFileWrapper.unwrap(jarURLConnection.getJarFile())).isSameAs(this.jarFile);
assertThat(jarURLConnection.getJarEntry()).isSameAs(this.jarFile.getJarEntry("1.dat"));
assertThat(jarURLConnection.getContentLength()).isOne();
assertThat(jarURLConnection.getContent()).isInstanceOf(InputStream.class);
assertThat(jarURLConnection.getContentType()).isEqualTo("content/unknown");
assertThat(jarURLConnection.getPermission()).isInstanceOf(FilePermission.class);
FilePermission permission = (FilePermission) jarURLConnection.getPermission();
assertThat(permission.getActions()).isEqualTo("read");
assertThat(permission.getName()).isEqualTo(this.rootJarFile.getPath());
}
@Test
void getMissingEntryUrl() throws Exception {
URL url = new URL(this.jarFile.getUrl(), "missing.dat");
assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/missing.dat");
assertThatExceptionOfType(FileNotFoundException.class)
.isThrownBy(((JarURLConnection) url.openConnection())::getJarEntry);
}
@Test
void getUrlStream() throws Exception {
URL url = this.jarFile.getUrl();
url.openConnection();
assertThatIOException().isThrownBy(url::openStream);
}
@Test
void getEntryUrlStream() throws Exception {
URL url = new URL(this.jarFile.getUrl(), "1.dat");
url.openConnection();
try (InputStream stream = url.openStream()) {
assertThat(stream.read()).isOne();
assertThat(stream.read()).isEqualTo(-1);
}
}
@Test
void getNestedJarFile() throws Exception {
try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
assertThat(nestedJarFile.getComment()).isEqualTo("nested");
Enumeration<java.util.jar.JarEntry> entries = nestedJarFile.entries();
assertThat(entries.nextElement().getName()).isEqualTo("META-INF/");
assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF");
assertThat(entries.nextElement().getName()).isEqualTo("3.dat");
assertThat(entries.nextElement().getName()).isEqualTo("4.dat");
assertThat(entries.nextElement().getName()).isEqualTo("\u00E4.dat");
assertThat(entries.hasMoreElements()).isFalse();
InputStream inputStream = nestedJarFile.getInputStream(nestedJarFile.getEntry("3.dat"));
assertThat(inputStream.read()).isEqualTo(3);
assertThat(inputStream.read()).isEqualTo(-1);
URL url = nestedJarFile.getUrl();
assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar!/");
JarURLConnection conn = (JarURLConnection) url.openConnection();
assertThat(JarFileWrapper.unwrap(conn.getJarFile())).isSameAs(nestedJarFile);
assertThat(conn.getJarFileURL()).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar");
assertThat(conn.getInputStream()).isNotNull();
JarInputStream jarInputStream = new JarInputStream(conn.getInputStream());
assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("3.dat");
assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("4.dat");
assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("\u00E4.dat");
jarInputStream.close();
assertThat(conn.getPermission()).isInstanceOf(FilePermission.class);
FilePermission permission = (FilePermission) conn.getPermission();
assertThat(permission.getActions()).isEqualTo("read");
assertThat(permission.getName()).isEqualTo(this.rootJarFile.getPath());
}
}
@Test
void getNestedJarDirectory() throws Exception {
try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("d/"))) {
Enumeration<java.util.jar.JarEntry> entries = nestedJarFile.entries();
assertThat(entries.nextElement().getName()).isEqualTo("9.dat");
assertThat(entries.hasMoreElements()).isFalse();
try (InputStream inputStream = nestedJarFile.getInputStream(nestedJarFile.getEntry("9.dat"))) {
assertThat(inputStream.read()).isEqualTo(9);
assertThat(inputStream.read()).isEqualTo(-1);
}
URL url = nestedJarFile.getUrl();
assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/d!/");
JarURLConnection connection = (JarURLConnection) url.openConnection();
assertThat(JarFileWrapper.unwrap(connection.getJarFile())).isSameAs(nestedJarFile);
}
}
@Test
void getNestedJarEntryUrl() throws Exception {
try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
URL url = nestedJarFile.getJarEntry("3.dat").getUrl();
assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat");
try (InputStream inputStream = url.openStream()) {
assertThat(inputStream).isNotNull();
assertThat(inputStream.read()).isEqualTo(3);
}
}
}
@Test
void createUrlFromString() throws Exception {
String spec = "jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat";
URL url = new URL(spec);
assertThat(url).hasToString(spec);
JarURLConnection connection = (JarURLConnection) url.openConnection();
try (InputStream inputStream = connection.getInputStream()) {
assertThat(inputStream).isNotNull();
assertThat(inputStream.read()).isEqualTo(3);
assertThat(connection.getURL()).hasToString(spec);
assertThat(connection.getJarFileURL()).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar");
assertThat(connection.getEntryName()).isEqualTo("3.dat");
connection.getJarFile().close();
}
}
@Test
void createNonNestedUrlFromString() throws Exception {
nonNestedJarFileFromString("jar:" + this.rootJarFile.toURI() + "!/2.dat");
}
@Test
void createNonNestedUrlFromPathString() throws Exception {
nonNestedJarFileFromString("jar:" + this.rootJarFile.toPath().toUri() + "!/2.dat");
}
private void nonNestedJarFileFromString(String spec) throws Exception {
JarFile.registerUrlProtocolHandler();
URL url = new URL(spec);
assertThat(url).hasToString(spec);
JarURLConnection connection = (JarURLConnection) url.openConnection();
try (InputStream inputStream = connection.getInputStream()) {
assertThat(inputStream).isNotNull();
assertThat(inputStream.read()).isEqualTo(2);
assertThat(connection.getURL()).hasToString(spec);
assertThat(connection.getJarFileURL().toURI()).isEqualTo(this.rootJarFile.toURI());
assertThat(connection.getEntryName()).isEqualTo("2.dat");
}
connection.getJarFile().close();
}
@Test
void getDirectoryInputStream() throws Exception {
InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("d/"));
assertThat(inputStream).isNotNull();
assertThat(inputStream.read()).isEqualTo(-1);
}
@Test
void getDirectoryInputStreamWithoutSlash() throws Exception {
InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("d"));
assertThat(inputStream).isNotNull();
assertThat(inputStream.read()).isEqualTo(-1);
}
@Test
void sensibleToString() throws Exception {
assertThat(this.jarFile).hasToString(this.rootJarFile.getPath());
try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
assertThat(nested).hasToString(this.rootJarFile.getPath() + "!/nested.jar");
}
}
@Test
void verifySignedJar() throws Exception {
File signedJarFile = getSignedJarFile();
assertThat(signedJarFile).exists();
try (java.util.jar.JarFile expected = new java.util.jar.JarFile(signedJarFile)) {
try (JarFile actual = new JarFile(signedJarFile)) {
StopWatch stopWatch = new StopWatch();
Enumeration<JarEntry> actualEntries = actual.entries();
while (actualEntries.hasMoreElements()) {
JarEntry actualEntry = actualEntries.nextElement();
java.util.jar.JarEntry expectedEntry = expected.getJarEntry(actualEntry.getName());
StreamUtils.drain(expected.getInputStream(expectedEntry));
if (!actualEntry.getName().equals("META-INF/MANIFEST.MF")) {
assertThat(actualEntry.getCertificates()).as(actualEntry.getName())
.isEqualTo(expectedEntry.getCertificates());
assertThat(actualEntry.getCodeSigners()).as(actualEntry.getName())
.isEqualTo(expectedEntry.getCodeSigners());
}
}
assertThat(stopWatch.getTotalTimeSeconds()).isLessThan(3.0);
}
}
}
private File getSignedJarFile() {
String[] entries = System.getProperty("java.class.path").split(System.getProperty("path.separator"));
for (String entry : entries) {
if (entry.contains("bcprov")) {
return new File(entry);
}
}
return null;
}
@Test
void jarFileWithScriptAtTheStart() throws Exception {
File file = new File(this.tempDir, "test.jar");
InputStream sourceJarContent = new FileInputStream(this.rootJarFile);
FileOutputStream outputStream = new FileOutputStream(file);
StreamUtils.copy("#/bin/bash", Charset.defaultCharset(), outputStream);
FileCopyUtils.copy(sourceJarContent, outputStream);
this.rootJarFile = file;
this.jarFile.close();
this.jarFile = new JarFile(file);
// Call some other tests to verify
getEntries();
getNestedJarFile();
}
@Test
void cannotLoadMissingJar() throws Exception {
// relates to gh-1070
try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
URL nestedUrl = nestedJarFile.getUrl();
URL url = new URL(nestedUrl, nestedJarFile.getUrl() + "missing.jar!/3.dat");
assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(url.openConnection()::getInputStream);
}
}
@Test
void registerUrlProtocolHandlerWithNoExistingRegistration() {
String original = System.getProperty(PROTOCOL_HANDLER);
try {
System.clearProperty(PROTOCOL_HANDLER);
JarFile.registerUrlProtocolHandler();
String protocolHandler = System.getProperty(PROTOCOL_HANDLER);
assertThat(protocolHandler).isEqualTo(HANDLERS_PACKAGE);
}
finally {
if (original == null) {
System.clearProperty(PROTOCOL_HANDLER);
}
else {
System.setProperty(PROTOCOL_HANDLER, original);
}
}
}
@Test
void registerUrlProtocolHandlerAddsToExistingRegistration() {
String original = System.getProperty(PROTOCOL_HANDLER);
try {
System.setProperty(PROTOCOL_HANDLER, "com.example");
JarFile.registerUrlProtocolHandler();
String protocolHandler = System.getProperty(PROTOCOL_HANDLER);
assertThat(protocolHandler).isEqualTo("com.example|" + HANDLERS_PACKAGE);
}
finally {
if (original == null) {
System.clearProperty(PROTOCOL_HANDLER);
}
else {
System.setProperty(PROTOCOL_HANDLER, original);
}
}
}
@Test
void jarFileCanBeDeletedOnceItHasBeenClosed() throws Exception {
File jar = new File(this.tempDir, "test.jar");
TestJarCreator.createTestJar(jar);
JarFile jf = new JarFile(jar);
jf.close();
assertThat(jar.delete()).isTrue();
}
@Test
void createUrlFromStringWithContextWhenNotFound() throws Exception {
// gh-12483
JarURLConnection.setUseFastExceptions(true);
try {
try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
URL context = nested.getUrl();
new URL(context, "jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat").openConnection()
.getInputStream()
.close();
assertThatExceptionOfType(FileNotFoundException.class)
.isThrownBy(new URL(context, "jar:" + this.rootJarFile.toURI() + "!/no.dat")
.openConnection()::getInputStream);
}
}
finally {
JarURLConnection.setUseFastExceptions(false);
}
}
@Test
void multiReleaseEntry() throws Exception {
try (JarFile multiRelease = this.jarFile.getNestedJarFile(this.jarFile.getEntry("multi-release.jar"))) {
ZipEntry entry = multiRelease.getEntry("multi-release.dat");
assertThat(entry.getName()).isEqualTo("multi-release.dat");
InputStream inputStream = multiRelease.getInputStream(entry);
assertThat(inputStream.available()).isOne();
assertThat(inputStream.read()).isEqualTo(Runtime.version().feature());
}
}
@Test
void zip64JarThatExceedsZipEntryLimitCanBeRead() throws Exception {
File zip64Jar = new File(this.tempDir, "zip64.jar");
FileCopyUtils.copy(zip64Jar(), zip64Jar);
try (JarFile zip64JarFile = new JarFile(zip64Jar)) {
List<JarEntry> entries = Collections.list(zip64JarFile.entries());
assertThat(entries).hasSize(65537);
for (int i = 0; i < entries.size(); i++) {
JarEntry entry = entries.get(i);
InputStream entryInput = zip64JarFile.getInputStream(entry);
assertThat(entryInput).hasContent("Entry " + (i + 1));
}
}
}
@Test
void zip64JarThatExceedsZipSizeLimitCanBeRead() throws Exception {
Assumptions.assumeTrue(this.tempDir.getFreeSpace() > 6 * 1024 * 1024 * 1024, "Insufficient disk space");
File zip64Jar = new File(this.tempDir, "zip64.jar");
File entry = new File(this.tempDir, "entry.dat");
CRC32 crc32 = new CRC32();
try (FileOutputStream entryOut = new FileOutputStream(entry)) {
byte[] data = new byte[1024 * 1024];
new Random().nextBytes(data);
for (int i = 0; i < 1024; i++) {
entryOut.write(data);
crc32.update(data);
}
}
try (JarOutputStream jarOutput = new JarOutputStream(new FileOutputStream(zip64Jar))) {
for (int i = 0; i < 6; i++) {
JarEntry storedEntry = new JarEntry("huge-" + i);
storedEntry.setSize(entry.length());
storedEntry.setCompressedSize(entry.length());
storedEntry.setCrc(crc32.getValue());
storedEntry.setMethod(ZipEntry.STORED);
jarOutput.putNextEntry(storedEntry);
try (FileInputStream entryIn = new FileInputStream(entry)) {
StreamUtils.copy(entryIn, jarOutput);
}
jarOutput.closeEntry();
}
}
try (JarFile zip64JarFile = new JarFile(zip64Jar)) {
assertThat(Collections.list(zip64JarFile.entries())).hasSize(6);
}
}
@Test
void nestedZip64JarCanBeRead() throws Exception {
File outer = new File(this.tempDir, "outer.jar");
try (JarOutputStream jarOutput = new JarOutputStream(new FileOutputStream(outer))) {
JarEntry nestedEntry = new JarEntry("nested-zip64.jar");
byte[] contents = zip64Jar();
nestedEntry.setSize(contents.length);
nestedEntry.setCompressedSize(contents.length);
CRC32 crc32 = new CRC32();
crc32.update(contents);
nestedEntry.setCrc(crc32.getValue());
nestedEntry.setMethod(ZipEntry.STORED);
jarOutput.putNextEntry(nestedEntry);
jarOutput.write(contents);
jarOutput.closeEntry();
}
try (JarFile outerJarFile = new JarFile(outer)) {
try (JarFile nestedZip64JarFile = outerJarFile
.getNestedJarFile(outerJarFile.getJarEntry("nested-zip64.jar"))) {
List<JarEntry> entries = Collections.list(nestedZip64JarFile.entries());
assertThat(entries).hasSize(65537);
for (int i = 0; i < entries.size(); i++) {
JarEntry entry = entries.get(i);
InputStream entryInput = nestedZip64JarFile.getInputStream(entry);
assertThat(entryInput).hasContent("Entry " + (i + 1));
}
}
}
}
private byte[] zip64Jar() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
JarOutputStream jarOutput = new JarOutputStream(bytes);
for (int i = 0; i < 65537; i++) {
jarOutput.putNextEntry(new JarEntry(i + ".dat"));
jarOutput.write(("Entry " + (i + 1)).getBytes(StandardCharsets.UTF_8));
jarOutput.closeEntry();
}
jarOutput.close();
return bytes.toByteArray();
}
@Test
void jarFileEntryWithEpochTimeOfZeroShouldNotFail() throws Exception {
File file = createJarFileWithEpochTimeOfZero();
try (JarFile jar = new JarFile(file)) {
Enumeration<java.util.jar.JarEntry> entries = jar.entries();
JarEntry entry = entries.nextElement();
assertThat(entry.getLastModifiedTime().toInstant()).isEqualTo(Instant.EPOCH);
assertThat(entry.getName()).isEqualTo("1.dat");
}
}
private File createJarFileWithEpochTimeOfZero() throws Exception {
File jarFile = new File(this.tempDir, "temp.jar");
FileOutputStream fileOutputStream = new FileOutputStream(jarFile);
String comment = "outer";
try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) {
jarOutputStream.setComment(comment);
JarEntry entry = new JarEntry("1.dat");
entry.setLastModifiedTime(FileTime.from(Instant.EPOCH));
jarOutputStream.putNextEntry(entry);
jarOutputStream.write(new byte[] { (byte) 1 });
jarOutputStream.closeEntry();
}
byte[] data = Files.readAllBytes(jarFile.toPath());
int headerPosition = data.length - ZipFile.ENDHDR - comment.getBytes().length;
int centralHeaderPosition = (int) Bytes.littleEndianValue(data, headerPosition + ZipFile.ENDOFF, 1);
int localHeaderPosition = (int) Bytes.littleEndianValue(data, centralHeaderPosition + ZipFile.CENOFF, 1);
writeTimeBlock(data, centralHeaderPosition + ZipFile.CENTIM, 0);
writeTimeBlock(data, localHeaderPosition + ZipFile.LOCTIM, 0);
File jar = new File(this.tempDir, "zerotimed.jar");
Files.write(jar.toPath(), data);
return jar;
}
private static void writeTimeBlock(byte[] data, int pos, int value) {
data[pos] = (byte) (value & 0xff);
data[pos + 1] = (byte) ((value >> 8) & 0xff);
data[pos + 2] = (byte) ((value >> 16) & 0xff);
data[pos + 3] = (byte) ((value >> 24) & 0xff);
}
@Test
void iterator() {
Iterator<JarEntry> iterator = this.jarFile.iterator();
List<String> names = new ArrayList<>();
while (iterator.hasNext()) {
names.add(iterator.next().getName());
}
assertThat(names).hasSize(12).contains("1.dat");
}
@Test
void iteratorWhenClosed() throws IOException {
this.jarFile.close();
assertThatZipFileClosedIsThrownBy(() -> this.jarFile.iterator());
}
@Test
void iteratorWhenClosedLater() throws IOException {
Iterator<JarEntry> iterator = this.jarFile.iterator();
iterator.next();
this.jarFile.close();
assertThatZipFileClosedIsThrownBy(() -> iterator.hasNext());
}
@Test
void stream() {
Stream<String> stream = this.jarFile.stream().map(JarEntry::getName);
assertThat(stream).hasSize(12).contains("1.dat");
}
private void assertThatZipFileClosedIsThrownBy(ThrowingCallable throwingCallable) {
assertThatIllegalStateException().isThrownBy(throwingCallable).withMessage("zip file closed");
}
}

View File

@@ -1,281 +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.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.Permission;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.Set;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.stream.Stream;
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.jar.JarFileWrapperTests.SpyJarFile.Call;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link JarFileWrapper}.
*
* @author Phillip Webb
*/
class JarFileWrapperTests {
private SpyJarFile parent;
private JarFileWrapper wrapper;
@BeforeEach
void setup(@TempDir File temp) throws Exception {
this.parent = new SpyJarFile(createTempJar(temp));
this.wrapper = new JarFileWrapper(this.parent);
}
@AfterEach
void cleanup() throws Exception {
this.parent.close();
}
private File createTempJar(File temp) throws IOException {
File file = new File(temp, "temp.jar");
new JarOutputStream(new FileOutputStream(file)).close();
return file;
}
@Test
void getUrlDelegatesToParent() throws MalformedURLException {
this.wrapper.getUrl();
this.parent.verify(Call.GET_URL);
}
@Test
void getTypeDelegatesToParent() {
this.wrapper.getType();
this.parent.verify(Call.GET_TYPE);
}
@Test
void getPermissionDelegatesToParent() {
this.wrapper.getPermission();
this.parent.verify(Call.GET_PERMISSION);
}
@Test
void getManifestDelegatesToParent() throws IOException {
this.wrapper.getManifest();
this.parent.verify(Call.GET_MANIFEST);
}
@Test
void entriesDelegatesToParent() {
this.wrapper.entries();
this.parent.verify(Call.ENTRIES);
}
@Test
void getJarEntryDelegatesToParent() {
this.wrapper.getJarEntry("test");
this.parent.verify(Call.GET_JAR_ENTRY);
}
@Test
void getEntryDelegatesToParent() {
this.wrapper.getEntry("test");
this.parent.verify(Call.GET_ENTRY);
}
@Test
void getInputStreamDelegatesToParent() throws IOException {
this.wrapper.getInputStream();
this.parent.verify(Call.GET_INPUT_STREAM);
}
@Test
void getEntryInputStreamDelegatesToParent() throws IOException {
ZipEntry entry = new ZipEntry("test");
this.wrapper.getInputStream(entry);
this.parent.verify(Call.GET_ENTRY_INPUT_STREAM);
}
@Test
void getCommentDelegatesToParent() {
this.wrapper.getComment();
this.parent.verify(Call.GET_COMMENT);
}
@Test
void sizeDelegatesToParent() {
this.wrapper.size();
this.parent.verify(Call.SIZE);
}
@Test
void toStringDelegatesToParent() {
assertThat(this.wrapper.toString()).endsWith("temp.jar");
}
@Test // gh-22991
void wrapperMustNotImplementClose() {
// If the wrapper overrides close then on Java 11 a FinalizableResource
// instance will be used to perform cleanup. This can result in a lot
// of additional memory being used since cleanup only occurs when the
// finalizer thread runs. See gh-22991
assertThatExceptionOfType(NoSuchMethodException.class)
.isThrownBy(() -> JarFileWrapper.class.getDeclaredMethod("close"));
}
@Test
void streamDelegatesToParent() {
this.wrapper.stream();
this.parent.verify(Call.STREAM);
}
/**
* {@link JarFile} that we can spy (even on Java 11+)
*/
static class SpyJarFile extends JarFile {
private final Set<Call> calls = EnumSet.noneOf(Call.class);
SpyJarFile(File file) throws IOException {
super(file);
}
@Override
Permission getPermission() {
mark(Call.GET_PERMISSION);
return super.getPermission();
}
@Override
public Manifest getManifest() throws IOException {
mark(Call.GET_MANIFEST);
return super.getManifest();
}
@Override
public Enumeration<java.util.jar.JarEntry> entries() {
mark(Call.ENTRIES);
return super.entries();
}
@Override
public Stream<java.util.jar.JarEntry> stream() {
mark(Call.STREAM);
return super.stream();
}
@Override
public JarEntry getJarEntry(String name) {
mark(Call.GET_JAR_ENTRY);
return super.getJarEntry(name);
}
@Override
public ZipEntry getEntry(String name) {
mark(Call.GET_ENTRY);
return super.getEntry(name);
}
@Override
InputStream getInputStream() throws IOException {
mark(Call.GET_INPUT_STREAM);
return super.getInputStream();
}
@Override
InputStream getInputStream(String name) throws IOException {
mark(Call.GET_ENTRY_INPUT_STREAM);
return super.getInputStream(name);
}
@Override
public String getComment() {
mark(Call.GET_COMMENT);
return super.getComment();
}
@Override
public int size() {
mark(Call.SIZE);
return super.size();
}
@Override
public URL getUrl() throws MalformedURLException {
mark(Call.GET_URL);
return super.getUrl();
}
@Override
JarFileType getType() {
mark(Call.GET_TYPE);
return super.getType();
}
private void mark(Call call) {
this.calls.add(call);
}
void verify(Call call) {
assertThat(call).matches(this.calls::contains);
}
enum Call {
GET_URL,
GET_TYPE,
GET_PERMISSION,
GET_MANIFEST,
ENTRIES,
GET_JAR_ENTRY,
GET_ENTRY,
GET_INPUT_STREAM,
GET_ENTRY_INPUT_STREAM,
GET_COMMENT,
SIZE,
STREAM
}
}
}

View File

@@ -1,246 +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.FileNotFoundException;
import java.io.InputStream;
import java.net.URL;
import java.util.List;
import java.util.jar.JarEntry;
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.jar.JarURLConnection.JarEntryName;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link JarURLConnection}.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @author Rostyslav Dudka
*/
class JarURLConnectionTests {
private File rootJarFile;
private JarFile jarFile;
@BeforeEach
void setup(@TempDir File tempDir) throws Exception {
this.rootJarFile = new File(tempDir, "root.jar");
TestJarCreator.createTestJar(this.rootJarFile);
this.jarFile = new JarFile(this.rootJarFile);
}
@AfterEach
void tearDown() throws Exception {
this.jarFile.close();
}
@Test
void connectionToRootUsingAbsoluteUrl() throws Exception {
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/");
Object content = JarURLConnection.get(url, this.jarFile).getContent();
assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) content)).isSameAs(this.jarFile);
}
@Test
void connectionToRootUsingRelativeUrl() throws Exception {
URL url = new URL("jar:file:" + getRelativePath() + "!/");
Object content = JarURLConnection.get(url, this.jarFile).getContent();
assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) content)).isSameAs(this.jarFile);
}
@Test
void connectionToEntryUsingAbsoluteUrl() throws Exception {
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/1.dat");
try (InputStream input = JarURLConnection.get(url, this.jarFile).getInputStream()) {
assertThat(input).hasBinaryContent(new byte[] { 1 });
}
}
@Test
void connectionToEntryUsingRelativeUrl() throws Exception {
URL url = new URL("jar:file:" + getRelativePath() + "!/1.dat");
try (InputStream input = JarURLConnection.get(url, this.jarFile).getInputStream()) {
assertThat(input).hasBinaryContent(new byte[] { 1 });
}
}
@Test
void connectionToEntryUsingAbsoluteUrlWithFileColonSlashSlashPrefix() throws Exception {
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/1.dat");
try (InputStream input = JarURLConnection.get(url, this.jarFile).getInputStream()) {
assertThat(input).hasBinaryContent(new byte[] { 1 });
}
}
@Test
void connectionToEntryUsingAbsoluteUrlForNestedEntry() throws Exception {
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat");
JarURLConnection connection = JarURLConnection.get(url, this.jarFile);
try (InputStream input = connection.getInputStream()) {
assertThat(input).hasBinaryContent(new byte[] { 3 });
}
connection.getJarFile().close();
}
@Test
void connectionToEntryUsingRelativeUrlForNestedEntry() throws Exception {
URL url = new URL("jar:file:" + getRelativePath() + "!/nested.jar!/3.dat");
JarURLConnection connection = JarURLConnection.get(url, this.jarFile);
try (InputStream input = connection.getInputStream()) {
assertThat(input).hasBinaryContent(new byte[] { 3 });
}
connection.getJarFile().close();
}
@Test
void connectionToEntryUsingAbsoluteUrlForEntryFromNestedJarFile() throws Exception {
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat");
try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
try (InputStream input = JarURLConnection.get(url, nested).getInputStream()) {
assertThat(input).hasBinaryContent(new byte[] { 3 });
}
}
}
@Test
void connectionToEntryUsingRelativeUrlForEntryFromNestedJarFile() throws Exception {
URL url = new URL("jar:file:" + getRelativePath() + "!/nested.jar!/3.dat");
try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
try (InputStream input = JarURLConnection.get(url, nested).getInputStream()) {
assertThat(input).hasBinaryContent(new byte[] { 3 });
}
}
}
@Test
void connectionToEntryInNestedJarFromUrlThatUsesExistingUrlAsContext() throws Exception {
URL url = new URL(new URL("jar", null, -1, this.rootJarFile.toURI().toURL() + "!/nested.jar!/", new Handler()),
"/3.dat");
try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
try (InputStream input = JarURLConnection.get(url, nested).getInputStream()) {
assertThat(input).hasBinaryContent(new byte[] { 3 });
}
}
}
@Test
void connectionToEntryWithSpaceNestedEntry() throws Exception {
URL url = new URL("jar:file:" + getRelativePath() + "!/space nested.jar!/3.dat");
JarURLConnection connection = JarURLConnection.get(url, this.jarFile);
try (InputStream input = connection.getInputStream()) {
assertThat(input).hasBinaryContent(new byte[] { 3 });
}
connection.getJarFile().close();
}
@Test
void connectionToEntryWithEncodedSpaceNestedEntry() throws Exception {
URL url = new URL("jar:file:" + getRelativePath() + "!/space%20nested.jar!/3.dat");
JarURLConnection connection = JarURLConnection.get(url, this.jarFile);
try (InputStream input = connection.getInputStream()) {
assertThat(input).hasBinaryContent(new byte[] { 3 });
}
connection.getJarFile().close();
}
@Test
void connectionToEntryUsingWrongAbsoluteUrlForEntryFromNestedJarFile() throws Exception {
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/w.jar!/3.dat");
try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
assertThatExceptionOfType(FileNotFoundException.class)
.isThrownBy(JarURLConnection.get(url, nested)::getInputStream);
}
}
@Test
void getContentLengthReturnsLengthOfUnderlyingEntry() throws Exception {
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat");
try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
JarURLConnection connection = JarURLConnection.get(url, nested);
assertThat(connection.getContentLength()).isOne();
}
}
@Test
void getContentLengthLongReturnsLengthOfUnderlyingEntry() throws Exception {
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat");
try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) {
JarURLConnection connection = JarURLConnection.get(url, nested);
assertThat(connection.getContentLengthLong()).isOne();
}
}
@Test
void getLastModifiedReturnsLastModifiedTimeOfJarEntry() throws Exception {
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/1.dat");
JarURLConnection connection = JarURLConnection.get(url, this.jarFile);
assertThat(connection.getLastModified()).isEqualTo(connection.getJarEntry().getTime());
}
@Test
void entriesCanBeStreamedFromJarFileOfConnection() throws Exception {
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/");
JarURLConnection connection = JarURLConnection.get(url, this.jarFile);
List<String> entryNames = connection.getJarFile().stream().map(JarEntry::getName).toList();
assertThat(entryNames).hasSize(12);
}
@Test
void jarEntryBasicName() {
assertThat(new JarEntryName(new StringSequence("a/b/C.class"))).hasToString("a/b/C.class");
}
@Test
void jarEntryNameWithSingleByteEncodedCharacters() {
assertThat(new JarEntryName(new StringSequence("%61/%62/%43.class"))).hasToString("a/b/C.class");
}
@Test
void jarEntryNameWithDoubleByteEncodedCharacters() {
assertThat(new JarEntryName(new StringSequence("%c3%a1/b/C.class"))).hasToString("\u00e1/b/C.class");
}
@Test
void jarEntryNameWithMixtureOfEncodedAndUnencodedDoubleByteCharacters() {
assertThat(new JarEntryName(new StringSequence("%c3%a1/b/\u00c7.class"))).hasToString("\u00e1/b/\u00c7.class");
}
@Test
void openConnectionCanBeClosedWithoutClosingSourceJar() throws Exception {
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/");
JarURLConnection connection = JarURLConnection.get(url, this.jarFile);
java.util.jar.JarFile connectionJarFile = connection.getJarFile();
connectionJarFile.close();
assertThat(this.jarFile.isClosed()).isFalse();
}
private String getRelativePath() {
return this.rootJarFile.getPath().replace('\\', '/');
}
}

View File

@@ -1,57 +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.lang.ref.SoftReference;
import java.util.Map;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.springframework.test.util.ReflectionTestUtils;
/**
* JUnit 5 {@link Extension} for tests that interact with Spring Boot's {@link Handler}
* for {@code jar:} URLs. Ensures that the handler is registered prior to test execution
* and cleans up the handler's root file cache afterwards.
*
* @author Andy Wilkinson
*/
class JarUrlProtocolHandler implements BeforeEachCallback, AfterEachCallback {
@Override
public void beforeEach(ExtensionContext context) throws Exception {
JarFile.registerUrlProtocolHandler();
}
@Override
@SuppressWarnings("unchecked")
public void afterEach(ExtensionContext context) throws Exception {
Map<File, JarFile> rootFileCache = ((SoftReference<Map<File, JarFile>>) ReflectionTestUtils
.getField(Handler.class, "rootFileCache")).get();
if (rootFileCache != null) {
for (JarFile rootJarFile : rootFileCache.values()) {
rootJarFile.close();
}
rootFileCache.clear();
}
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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.Name;
import java.util.jar.Manifest;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ManifestInfo}.
*
* @author Phillip Webb
*/
class ManifestInfoTests {
@Test
void noneReturnsNoDetails() {
assertThat(ManifestInfo.NONE.getManifest()).isNull();
assertThat(ManifestInfo.NONE.isMultiRelease()).isFalse();
}
@Test
void getManifestReturnsManifest() {
Manifest manifest = new Manifest();
ManifestInfo info = new ManifestInfo(manifest);
assertThat(info.getManifest()).isSameAs(manifest);
}
@Test
void isMultiReleaseWhenHasMultiReleaseAttributeReturnsTrue() {
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(new Name("Multi-Release"), "true");
ManifestInfo info = new ManifestInfo(manifest);
assertThat(info.isMultiRelease()).isTrue();
}
@Test
void isMultiReleaseWhenHasNoMultiReleaseAttributeReturnsFalse() {
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(new Name("Random-Release"), "true");
ManifestInfo info = new ManifestInfo(manifest);
assertThat(info.isMultiRelease()).isFalse();
}
}

View File

@@ -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.jar;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.boot.loader.zip.ZipContent;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link MetaInfVersionsInfo}.
*
* @author Phillip Webb
*/
class MetaInfVersionsInfoTests {
@Test
void getParsesVersionsAndEntries() {
List<ZipContent.Entry> entries = new ArrayList<>();
entries.add(mockEntry("META-INF/"));
entries.add(mockEntry("META-INF/MANIFEST.MF"));
entries.add(mockEntry("META-INF/versions/"));
entries.add(mockEntry("META-INF/versions/9/"));
entries.add(mockEntry("META-INF/versions/9/Foo.class"));
entries.add(mockEntry("META-INF/versions/11/"));
entries.add(mockEntry("META-INF/versions/11/Foo.class"));
entries.add(mockEntry("META-INF/versions/10/"));
entries.add(mockEntry("META-INF/versions/10/Foo.class"));
MetaInfVersionsInfo info = MetaInfVersionsInfo.get(entries.size(), entries::get);
assertThat(info.versions()).containsExactly(9, 10, 11);
assertThat(info.directories()).containsExactly("META-INF/versions/9/", "META-INF/versions/10/",
"META-INF/versions/11/");
}
@Test
void getWhenHasBadEntryParsesGoodVersionsAndEntries() {
List<ZipContent.Entry> entries = new ArrayList<>();
entries.add(mockEntry("META-INF/versions/9/Foo.class"));
entries.add(mockEntry("META-INF/versions/0x11/Foo.class"));
MetaInfVersionsInfo info = MetaInfVersionsInfo.get(entries.size(), entries::get);
assertThat(info.versions()).containsExactly(9);
assertThat(info.directories()).containsExactly("META-INF/versions/9/");
}
@Test
void getWhenHasNoEntriesReturnsNone() {
List<ZipContent.Entry> entries = new ArrayList<>();
MetaInfVersionsInfo info = MetaInfVersionsInfo.get(entries.size(), entries::get);
assertThat(info.versions()).isEmpty();
assertThat(info.directories()).isEmpty();
assertThat(info).isSameAs(MetaInfVersionsInfo.NONE);
}
private ZipContent.Entry mockEntry(String name) {
ZipContent.Entry entry = mock(ZipContent.Entry.class);
given(entry.getName()).willReturn(name);
given(entry.hasNameStartingWith(any()))
.willAnswer((invocation) -> name.startsWith(invocation.getArgument(0, CharSequence.class).toString()));
given(entry.isDirectory()).willAnswer((invocation) -> name.endsWith("/"));
return entry;
}
}

View File

@@ -0,0 +1,368 @@
/*
* 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.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.Cleaner.Cleanable;
import java.nio.charset.Charset;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipFile;
import org.assertj.core.extractor.Extractors;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.loader.ref.Cleaner;
import org.springframework.boot.loader.testsupport.TestJar;
import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed;
import org.springframework.boot.loader.zip.ZipContent;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StopWatch;
import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.atMostOnce;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link NestedJarFile}.
*
* @author Phillip Webb
* @author Martin Lau
* @author Andy Wilkinson
* @author Madhura Bhave
*/
@AssertFileChannelDataBlocksClosed
class NestedJarFileTests {
@TempDir
File tempDir;
private File file;
@BeforeEach
void setup() throws Exception {
this.file = new File(this.tempDir, "test.jar");
TestJar.create(this.file);
}
@Test
void createOpensJar() throws IOException {
try (NestedJarFile jar = new NestedJarFile(this.file)) {
try (JarFile jdkJar = new JarFile(this.file)) {
assertThat(jar.size()).isEqualTo(jdkJar.size());
assertThat(jar.getComment()).isEqualTo(jdkJar.getComment());
Enumeration<JarEntry> entries = jar.entries();
Enumeration<JarEntry> jdkEntries = jdkJar.entries();
while (entries.hasMoreElements()) {
assertThat(entries.nextElement().getName()).isEqualTo(jdkEntries.nextElement().getName());
}
assertThat(jdkEntries.hasMoreElements()).isFalse();
try (InputStream in = jar.getInputStream(jar.getEntry("1.dat"))) {
assertThat(in.readAllBytes()).containsExactly(new byte[] { 1 });
}
}
}
}
@Test
void createWhenNestedJarFileOpensJar() throws IOException {
try (NestedJarFile jar = new NestedJarFile(this.file, "nested.jar")) {
assertThat(jar.size()).isEqualTo(5);
assertThat(jar.stream().map(JarEntry::getName)).containsExactly("META-INF/", "META-INF/MANIFEST.MF",
"3.dat", "4.dat", "\u00E4.dat");
}
}
@Test
void createWhenNestedJarDirectoryOpensJar() throws IOException {
try (NestedJarFile jar = new NestedJarFile(this.file, "d/")) {
assertThat(jar.getName()).isEqualTo(this.file.getAbsolutePath() + "!/d/");
assertThat(jar.size()).isEqualTo(3);
assertThat(jar.stream().map(JarEntry::getName)).containsExactly("META-INF/", "META-INF/MANIFEST.MF",
"9.dat");
}
}
@Test
void createWhenJarHasFrontMatterOpensJar() throws IOException {
File file = new File(this.tempDir, "frontmatter.jar");
InputStream sourceJarContent = new FileInputStream(this.file);
FileOutputStream outputStream = new FileOutputStream(file);
StreamUtils.copy("#/bin/bash", Charset.defaultCharset(), outputStream);
FileCopyUtils.copy(sourceJarContent, outputStream);
try (NestedJarFile jar = new NestedJarFile(file)) {
assertThat(jar.size()).isEqualTo(12);
}
try (NestedJarFile jar = new NestedJarFile(this.file, "nested.jar")) {
assertThat(jar.size()).isEqualTo(5);
}
}
@Test
void getEntryReturnsEntry() throws IOException {
try (NestedJarFile jar = new NestedJarFile(this.file)) {
JarEntry entry = jar.getEntry("1.dat");
assertEntryOne(entry);
}
}
@Test
void getEntryWhenClosedThrowsException() throws IOException {
try (NestedJarFile jar = new NestedJarFile(this.file)) {
jar.close();
assertThatIllegalStateException().isThrownBy(() -> jar.getEntry("1.dat")).withMessage("Zip file closed");
}
}
@Test
void getJarEntryReturnsEntry() throws IOException {
try (NestedJarFile jar = new NestedJarFile(this.file)) {
JarEntry entry = jar.getJarEntry("1.dat");
assertEntryOne(entry);
}
}
@Test
void getJarEntryWhenClosedThrowsException() throws IOException {
try (NestedJarFile jar = new NestedJarFile(this.file)) {
jar.close();
assertThatIllegalStateException().isThrownBy(() -> jar.getJarEntry("1.dat")).withMessage("Zip file closed");
}
}
private void assertEntryOne(JarEntry entry) {
assertThat(entry.getName()).isEqualTo("1.dat");
assertThat(entry.getRealName()).isEqualTo("1.dat");
assertThat(entry.getSize()).isEqualTo(1);
assertThat(entry.getCompressedSize()).isEqualTo(3);
assertThat(entry.getCrc()).isEqualTo(2768625435L);
assertThat(entry.getMethod()).isEqualTo(8);
}
@Test
void getEntryWhenMultiReleaseEntryReturnsEntry() throws IOException {
File multiReleaseFile = new File(this.tempDir, "mutli.zip");
try (ZipContent zip = ZipContent.open(this.file.toPath(), "multi-release.jar")) {
try (InputStream in = zip.openRawZipData().asInputStream()) {
try (FileOutputStream out = new FileOutputStream(multiReleaseFile)) {
in.transferTo(out);
}
}
}
try (NestedJarFile jar = new NestedJarFile(this.file, "multi-release.jar", JarFile.runtimeVersion())) {
try (JarFile jdkJar = new JarFile(multiReleaseFile, true, ZipFile.OPEN_READ, JarFile.runtimeVersion())) {
JarEntry entry = jar.getJarEntry("multi-release.dat");
JarEntry jdkEntry = jdkJar.getJarEntry("multi-release.dat");
assertThat(entry.getName()).isEqualTo(jdkEntry.getName());
assertThat(entry.getRealName()).isEqualTo(jdkEntry.getRealName());
try (InputStream inputStream = jdkJar.getInputStream(entry)) {
assertThat(inputStream.available()).isOne();
assertThat(inputStream.read()).isEqualTo(Runtime.version().feature());
}
try (InputStream inputStream = jar.getInputStream(entry)) {
assertThat(inputStream.available()).isOne();
assertThat(inputStream.read()).isEqualTo(Runtime.version().feature());
}
}
}
}
@Test
void getManifestReturnsManifest() throws IOException {
try (NestedJarFile jar = new NestedJarFile(this.file)) {
Manifest manifest = jar.getManifest();
assertThat(manifest).isNotNull();
assertThat(manifest.getEntries()).isEmpty();
assertThat(manifest.getMainAttributes().getValue("Manifest-Version")).isEqualTo("1.0");
}
}
@Test
void getCommentReturnsComment() throws IOException {
try (NestedJarFile jar = new NestedJarFile(this.file)) {
assertThat(jar.getComment()).isEqualTo("outer");
}
}
@Test
void getCommentWhenClosedThrowsException() throws IOException {
try (NestedJarFile jar = new NestedJarFile(this.file)) {
jar.close();
assertThatIllegalStateException().isThrownBy(() -> jar.getComment()).withMessage("Zip file closed");
}
}
@Test
void getNameReturnsName() throws IOException {
try (NestedJarFile jar = new NestedJarFile(this.file)) {
assertThat(jar.getName()).isEqualTo(this.file.getAbsolutePath());
}
}
@Test
void getNameWhenNestedReturnsName() throws IOException {
try (NestedJarFile jar = new NestedJarFile(this.file, "nested.jar")) {
assertThat(jar.getName()).isEqualTo(this.file.getAbsolutePath() + "!/nested.jar");
}
}
@Test
void sizeReturnsSize() throws IOException {
try (NestedJarFile jar = new NestedJarFile(this.file)) {
assertThat(jar.size()).isEqualByComparingTo(12);
}
}
@Test
void sizeWhenClosedThowsException() throws Exception {
try (NestedJarFile jar = new NestedJarFile(this.file)) {
jar.close();
assertThatIllegalStateException().isThrownBy(() -> jar.size()).withMessage("Zip file closed");
}
}
@Test
void getEntryTime() throws IOException {
try (NestedJarFile jar = new NestedJarFile(this.file)) {
try (JarFile jdkJar = new JarFile(this.file)) {
assertThat(jar.getEntry("META-INF/MANIFEST.MF").getTime())
.isEqualTo(jar.getEntry("META-INF/MANIFEST.MF").getTime());
}
}
}
@Test
void closeTriggersCleanupOnlyOnce() throws IOException {
Cleaner cleaner = mock(Cleaner.class);
ArgumentCaptor<Runnable> action = ArgumentCaptor.forClass(Runnable.class);
Cleanable cleanable = mock(Cleanable.class);
given(cleaner.register(any(), action.capture())).willReturn(cleanable);
NestedJarFile jar = new NestedJarFile(this.file, null, null, false, cleaner);
jar.close();
jar.close();
then(cleanable).should(atMostOnce()).clean();
action.getValue().run();
}
@Test
void cleanupFromReleasesResources() throws IOException {
Cleaner cleaner = mock(Cleaner.class);
ArgumentCaptor<Runnable> action = ArgumentCaptor.forClass(Runnable.class);
Cleanable cleanable = mock(Cleanable.class);
given(cleaner.register(any(), action.capture())).willReturn(cleanable);
try (NestedJarFile jar = new NestedJarFile(this.file, null, null, false, cleaner)) {
Object channel = Extractors.byName("resources.zipContent.data.channel").apply(jar);
assertThat(channel).extracting("referenceCount").isEqualTo(1);
action.getValue().run();
assertThat(channel).extracting("referenceCount").isEqualTo(0);
}
}
@Test
void getInputStreamReturnsInputStream() throws IOException {
try (NestedJarFile jarFile = new NestedJarFile(this.file)) {
JarEntry entry = jarFile.getJarEntry("2.dat");
try (InputStream in = jarFile.getInputStream(entry)) {
assertThat(in).hasBinaryContent(new byte[] { 0x02 });
}
}
}
@Test
void getInputStreamWhenIsDirectory() throws IOException {
try (NestedJarFile jar = new NestedJarFile(this.file)) {
try (InputStream inputStream = jar.getInputStream(jar.getEntry("d/"))) {
assertThat(inputStream).isNotNull();
assertThat(inputStream.read()).isEqualTo(-1);
}
}
}
@Test
void getInputStreamWhenNameWithoutSlashAndIsDirectory() throws IOException {
try (NestedJarFile jar = new NestedJarFile(this.file)) {
try (InputStream inputStream = jar.getInputStream(jar.getEntry("d"))) {
assertThat(inputStream).isNotNull();
assertThat(inputStream.read()).isEqualTo(-1);
}
}
}
@Test
void verifySignedJar() throws Exception {
File signedJarFile = TestJar.getSigned();
assertThat(signedJarFile).exists();
try (JarFile expected = new JarFile(signedJarFile)) {
try (NestedJarFile actual = new NestedJarFile(signedJarFile)) {
StopWatch stopWatch = new StopWatch();
Enumeration<JarEntry> actualEntries = actual.entries();
while (actualEntries.hasMoreElements()) {
JarEntry actualEntry = actualEntries.nextElement();
JarEntry expectedEntry = expected.getJarEntry(actualEntry.getName());
StreamUtils.drain(expected.getInputStream(expectedEntry));
if (!actualEntry.getName().equals("META-INF/MANIFEST.MF")) {
assertThat(actualEntry.getCertificates()).as(actualEntry.getName())
.isEqualTo(expectedEntry.getCertificates());
assertThat(actualEntry.getCodeSigners()).as(actualEntry.getName())
.isEqualTo(expectedEntry.getCodeSigners());
}
}
assertThat(stopWatch.getTotalTimeSeconds()).isLessThan(3.0);
}
}
}
@Test
void closeAllowsFileToBeDeleted() throws Exception {
new NestedJarFile(this.file).close();
assertThat(this.file.delete()).isTrue();
}
@Test
void streamStreamsEnties() throws IOException {
try (NestedJarFile jar = new NestedJarFile(this.file, "multi-release.jar")) {
assertThat(jar.stream().map((entry) -> entry.getName() + ":" + entry.getRealName())).containsExactly(
"META-INF/:META-INF/", "META-INF/MANIFEST.MF:META-INF/MANIFEST.MF",
"multi-release.dat:multi-release.dat",
"META-INF/versions/17/multi-release.dat:META-INF/versions/17/multi-release.dat");
}
}
@Test
void versionedStreamStreamsEntries() throws IOException {
try (NestedJarFile jar = new NestedJarFile(this.file, "multi-release.jar", Runtime.version())) {
assertThat(jar.versionedStream().map((entry) -> entry.getName() + ":" + entry.getRealName()))
.containsExactly("META-INF/:META-INF/", "META-INF/MANIFEST.MF:META-INF/MANIFEST.MF",
"multi-release.dat:META-INF/versions/17/multi-release.dat");
}
}
}

View File

@@ -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.jar;
import java.io.File;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.loader.testsupport.TestJar;
import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed;
import org.springframework.boot.loader.zip.ZipContent;
import org.springframework.boot.loader.zip.ZipContent.Entry;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SecurityInfo}.
*
* @author Phillip Webb
*/
@AssertFileChannelDataBlocksClosed
class SecurityInfoTests {
@TempDir
File temp;
@Test
void getWhenNoSignatureFileReturnsNone() throws Exception {
File file = new File(this.temp, "test.jar");
TestJar.create(file);
try (ZipContent content = ZipContent.open(file.toPath())) {
SecurityInfo info = SecurityInfo.get(content);
assertThat(info).isSameAs(SecurityInfo.NONE);
for (int i = 0; i < content.size(); i++) {
Entry entry = content.getEntry(i);
assertThat(info.getCertificates(entry)).isNull();
assertThat(info.getCodeSigners(entry)).isNull();
}
}
}
@Test
void getWhenHasSignatureFileButNoSecuityMaterialReturnsNone() throws Exception {
File file = new File(this.temp, "test.jar");
TestJar.create(file, false, true);
try (ZipContent content = ZipContent.open(file.toPath())) {
assertThat(content.hasJarSignatureFile()).isTrue();
SecurityInfo info = SecurityInfo.get(content);
assertThat(info).isSameAs(SecurityInfo.NONE);
}
}
@Test
void getWhenJarIsSigned() throws Exception {
File file = TestJar.getSigned();
try (ZipContent content = ZipContent.open(file.toPath())) {
assertThat(content.hasJarSignatureFile()).isTrue();
SecurityInfo info = SecurityInfo.get(content);
for (int i = 0; i < content.size(); i++) {
Entry entry = content.getEntry(i);
if (entry.getName().endsWith(".class")) {
assertThat(info.getCertificates(entry)).isNotNull();
assertThat(info.getCodeSigners(entry)).isNotNull();
}
}
}
}
}

View File

@@ -1,220 +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 org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
/**
* Tests for {@link StringSequence}.
*
* @author Phillip Webb
*/
class StringSequenceTests {
@Test
void createWhenSourceIsNullShouldThrowException() {
assertThatNullPointerException().isThrownBy(() -> new StringSequence(null))
.withMessage("Source must not be null");
}
@Test
void createWithIndexWhenSourceIsNullShouldThrowException() {
assertThatNullPointerException().isThrownBy(() -> new StringSequence(null, 0, 0))
.withMessage("Source must not be null");
}
@Test
void createWhenStartIsLessThanZeroShouldThrowException() {
assertThatExceptionOfType(StringIndexOutOfBoundsException.class)
.isThrownBy(() -> new StringSequence("x", -1, 0));
}
@Test
void createWhenEndIsGreaterThanLengthShouldThrowException() {
assertThatExceptionOfType(StringIndexOutOfBoundsException.class)
.isThrownBy(() -> new StringSequence("x", 0, 2));
}
@Test
void createFromString() {
assertThat(new StringSequence("test")).hasToString("test");
}
@Test
void subSequenceWithJustStartShouldReturnSubSequence() {
assertThat(new StringSequence("smiles").subSequence(1)).hasToString("miles");
}
@Test
void subSequenceShouldReturnSubSequence() {
assertThat(new StringSequence("hamburger").subSequence(4, 8)).hasToString("urge");
assertThat(new StringSequence("smiles").subSequence(1, 5)).hasToString("mile");
}
@Test
void subSequenceWhenCalledMultipleTimesShouldReturnSubSequence() {
assertThat(new StringSequence("hamburger").subSequence(4, 8).subSequence(1, 3)).hasToString("rg");
}
@Test
void subSequenceWhenEndPastExistingEndShouldThrowException() {
StringSequence sequence = new StringSequence("abcde").subSequence(1, 4);
assertThat(sequence).hasToString("bcd");
assertThat(sequence.subSequence(2, 3)).hasToString("d");
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> sequence.subSequence(3, 4));
}
@Test
void subSequenceWhenStartPastExistingEndShouldThrowException() {
StringSequence sequence = new StringSequence("abcde").subSequence(1, 4);
assertThat(sequence).hasToString("bcd");
assertThat(sequence.subSequence(2, 3)).hasToString("d");
assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> sequence.subSequence(4, 3));
}
@Test
void isEmptyWhenEmptyShouldReturnTrue() {
assertThat(new StringSequence("").isEmpty()).isTrue();
}
@Test
void isEmptyWhenNotEmptyShouldReturnFalse() {
assertThat(new StringSequence("x").isEmpty()).isFalse();
}
@Test
void lengthShouldReturnLength() {
StringSequence sequence = new StringSequence("hamburger");
assertThat(sequence).hasSize(9);
assertThat(sequence.subSequence(4, 8)).hasSize(4);
}
@Test
void charAtShouldReturnChar() {
StringSequence sequence = new StringSequence("hamburger");
assertThat(sequence.charAt(0)).isEqualTo('h');
assertThat(sequence.charAt(1)).isEqualTo('a');
assertThat(sequence.subSequence(4, 8).charAt(0)).isEqualTo('u');
assertThat(sequence.subSequence(4, 8).charAt(1)).isEqualTo('r');
}
@Test
void indexOfCharShouldReturnIndexOf() {
StringSequence sequence = new StringSequence("aabbaacc");
assertThat(sequence.indexOf('a')).isZero();
assertThat(sequence.indexOf('b')).isEqualTo(2);
assertThat(sequence.subSequence(2).indexOf('a')).isEqualTo(2);
}
@Test
void indexOfStringShouldReturnIndexOf() {
StringSequence sequence = new StringSequence("aabbaacc");
assertThat(sequence.indexOf('a')).isZero();
assertThat(sequence.indexOf('b')).isEqualTo(2);
assertThat(sequence.subSequence(2).indexOf('a')).isEqualTo(2);
}
@Test
void indexOfStringFromIndexShouldReturnIndexOf() {
StringSequence sequence = new StringSequence("aabbaacc");
assertThat(sequence.indexOf("a", 2)).isEqualTo(4);
assertThat(sequence.indexOf("b", 3)).isEqualTo(3);
assertThat(sequence.subSequence(2).indexOf("a", 3)).isEqualTo(3);
}
@Test
void hashCodeShouldBeSameAsString() {
assertThat(new StringSequence("hamburger")).hasSameHashCodeAs("hamburger");
assertThat(new StringSequence("hamburger").subSequence(4, 8)).hasSameHashCodeAs("urge");
}
@Test
void equalsWhenSameContentShouldMatch() {
StringSequence a = new StringSequence("hamburger").subSequence(4, 8);
StringSequence b = new StringSequence("urge");
StringSequence c = new StringSequence("urgh");
assertThat(a).isEqualTo(b).isNotEqualTo(c);
}
@Test
void notEqualsWhenSequencesOfDifferentLength() {
StringSequence a = new StringSequence("abcd");
StringSequence b = new StringSequence("ef");
assertThat(a).isNotEqualTo(b);
}
@Test
void startsWithWhenExactMatch() {
assertThat(new StringSequence("abc").startsWith("abc")).isTrue();
}
@Test
void startsWithWhenLongerAndStartsWith() {
assertThat(new StringSequence("abcd").startsWith("abc")).isTrue();
}
@Test
void startsWithWhenLongerAndDoesNotStartWith() {
assertThat(new StringSequence("abcd").startsWith("abx")).isFalse();
}
@Test
void startsWithWhenShorterAndDoesNotStartWith() {
assertThat(new StringSequence("ab").startsWith("abc")).isFalse();
assertThat(new StringSequence("ab").startsWith("c")).isFalse();
}
@Test
void startsWithOffsetWhenExactMatch() {
assertThat(new StringSequence("xabc").startsWith("abc", 1)).isTrue();
}
@Test
void startsWithOffsetWhenLongerAndStartsWith() {
assertThat(new StringSequence("xabcd").startsWith("abc", 1)).isTrue();
}
@Test
void startsWithOffsetWhenLongerAndDoesNotStartWith() {
assertThat(new StringSequence("xabcd").startsWith("abx", 1)).isFalse();
}
@Test
void startsWithOffsetWhenShorterAndDoesNotStartWith() {
assertThat(new StringSequence("xab").startsWith("abc", 1)).isFalse();
assertThat(new StringSequence("xab").startsWith("c", 1)).isFalse();
}
@Test
void startsWithOnSubstringTailWhenMatch() {
StringSequence subSequence = new StringSequence("xabc").subSequence(1);
assertThat(subSequence.startsWith("abc")).isTrue();
assertThat(subSequence.startsWith("abcd")).isFalse();
}
@Test
void startsWithOnSubstringMiddleWhenMatch() {
StringSequence subSequence = new StringSequence("xabc").subSequence(1, 3);
assertThat(subSequence.startsWith("ab")).isTrue();
assertThat(subSequence.startsWith("abc")).isFalse();
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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.jarmode;
import java.util.Arrays;
/**
* {@link JarMode} for testing.
*
* @author Phillip Webb
*/
class TestJarMode implements JarMode {
@Override
public boolean accepts(String mode) {
return "test".equals(mode);
}
@Override
public void run(String mode, String[] args) {
System.out.println("running in " + mode + " jar mode " + Arrays.asList(args));
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.loader;
package org.springframework.boot.loader.launch;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -27,9 +27,7 @@ import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
@@ -39,7 +37,6 @@ import java.util.zip.ZipEntry;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.util.FileCopyUtils;
/**
@@ -49,7 +46,7 @@ import org.springframework.util.FileCopyUtils;
* @author Madhura Bhave
* @author Scott Frederick
*/
public abstract class AbstractExecutableArchiveLauncherTests {
abstract class AbstractExecutableArchiveLauncherTests {
@TempDir
File tempDir;
@@ -58,13 +55,11 @@ public abstract class AbstractExecutableArchiveLauncherTests {
return createJarArchive(name, entryPrefix, false, Collections.emptyList());
}
@SuppressWarnings("resource")
protected File createJarArchive(String name, String entryPrefix, boolean indexed, List<String> extraLibs)
throws IOException {
return createJarArchive(name, null, entryPrefix, indexed, extraLibs);
}
@SuppressWarnings("resource")
protected File createJarArchive(String name, Manifest manifest, String entryPrefix, boolean indexed,
List<String> extraLibs) throws IOException {
File archive = new File(this.tempDir, name);
@@ -129,14 +124,6 @@ public abstract class AbstractExecutableArchiveLauncherTests {
return exploded;
}
protected Set<URL> getUrls(List<Archive> archives) throws MalformedURLException {
Set<URL> urls = new LinkedHashSet<>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
return urls;
}
protected final URL toUrl(File file) {
try {
return file.toURI().toURL();

View File

@@ -0,0 +1,111 @@
/*
* 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.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.function.Predicate;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.loader.launch.Archive.Entry;
import org.springframework.boot.loader.testsupport.TestJar;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.withSettings;
/**
* Tests for {@link Archive}.
*
* @author Phillip Webb
*/
class ArchiveTests {
@TempDir
File temp;
@Test
void getClassPathUrlsWithOnlyIncludeFilterSearchesAllDirectories() throws Exception {
Archive archive = mock(Archive.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));
Predicate<Entry> includeFilter = (entry) -> false;
archive.getClassPathUrls(includeFilter);
then(archive).should().getClassPathUrls(includeFilter, Archive.ALL_ENTRIES);
}
@Test
void isExplodedWhenHasRootDirectoryReturnsTrue() {
Archive archive = mock(Archive.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));
given(archive.getRootDirectory()).willReturn(this.temp);
assertThat(archive.isExploded()).isTrue();
}
@Test
void isExplodedWhenHasNoRootDirectoryReturnsFalse() {
Archive archive = mock(Archive.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));
given(archive.getRootDirectory()).willReturn(null);
assertThat(archive.isExploded()).isFalse();
}
@Test
void createFromProtectionDomainCreatesJarArchive() throws Exception {
File jarFile = new File(this.temp, "test.jar");
TestJar.create(jarFile);
ProtectionDomain protectionDomain = mock(ProtectionDomain.class);
CodeSource codeSource = mock(CodeSource.class);
given(protectionDomain.getCodeSource()).willReturn(codeSource);
given(codeSource.getLocation()).willReturn(jarFile.toURI().toURL());
Archive archive = Archive.create(protectionDomain);
assertThat(archive).isInstanceOf(JarFileArchive.class);
}
@Test
void createFromProtectionDomainWhenNoLocationThrowsException() throws Exception {
File jarFile = new File(this.temp, "test.jar");
TestJar.create(jarFile);
ProtectionDomain protectionDomain = mock(ProtectionDomain.class);
assertThatIllegalStateException().isThrownBy(() -> Archive.create(protectionDomain))
.withMessage("Unable to determine code source archive");
}
@Test
void createFromFileWhenFileDoesNotExistThrowsException() {
File target = new File(this.temp, "missing");
assertThatIllegalStateException().isThrownBy(() -> Archive.create(target))
.withMessageContaining("Unable to determine code source archive");
}
@Test
void createFromFileWhenJarFileReturnsJarFileArchive() throws Exception {
File target = new File(this.temp, "missing");
TestJar.create(target);
assertThat(Archive.create(target)).isInstanceOf(JarFileArchive.class);
}
@Test
void createFromFileWhenDirectoryReturnsExplodedFileArchive() throws Exception {
File target = this.temp;
assertThat(Archive.create(target)).isInstanceOf(ExplodedArchive.class);
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.loader;
package org.springframework.boot.loader.launch;
import java.io.File;
import java.io.IOException;
@@ -28,7 +28,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link ClassPathIndexFile}.
@@ -41,24 +40,17 @@ class ClassPathIndexFileTests {
@TempDir
File temp;
@Test
void loadIfPossibleWhenRootIsNotFileReturnsNull() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ClassPathIndexFile.loadIfPossible(new URL("https://example.com/file"), "test.idx"))
.withMessage("URL does not reference a file");
}
@Test
void loadIfPossibleWhenRootDoesNotExistReturnsNull() throws Exception {
File root = new File(this.temp, "missing");
assertThat(ClassPathIndexFile.loadIfPossible(root.toURI().toURL(), "test.idx")).isNull();
assertThat(ClassPathIndexFile.loadIfPossible(root, "test.idx")).isNull();
}
@Test
void loadIfPossibleWhenRootIsDirectoryThrowsException() throws Exception {
File root = new File(this.temp, "directory");
root.mkdirs();
assertThat(ClassPathIndexFile.loadIfPossible(root.toURI().toURL(), "test.idx")).isNull();
assertThat(ClassPathIndexFile.loadIfPossible(root, "test.idx")).isNull();
}
@Test
@@ -97,7 +89,7 @@ class ClassPathIndexFileTests {
private ClassPathIndexFile copyAndLoadTestIndexFile() throws IOException {
copyTestIndexFile();
ClassPathIndexFile indexFile = ClassPathIndexFile.loadIfPossible(this.temp.toURI().toURL(), "test.idx");
ClassPathIndexFile indexFile = ClassPathIndexFile.loadIfPossible(this.temp, "test.idx");
return indexFile;
}

View File

@@ -0,0 +1,159 @@
/*
* 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.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Set;
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.launch.Archive.Entry;
import org.springframework.boot.loader.testsupport.TestJar;
import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed;
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
*/
@AssertFileChannelDataBlocksClosed
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();
}
}
@Test
void isExplodedReturnsTrue() {
assertThat(this.archive.isExploded()).isTrue();
}
@Test
void getRootDirectoryReturnsRootDirectory() {
assertThat(this.archive.getRootDirectory()).isEqualTo(this.rootDirectory);
}
@Test
void getManifestReturnsManifest() throws Exception {
assertThat(this.archive.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1");
}
@Test
void getClassPathUrlsWhenNoPredicartesReturnsUrls() throws Exception {
Set<URL> urls = this.archive.getClassPathUrls(Archive.ALL_ENTRIES);
URL[] expectedUrls = TestJar.expectedEntries().stream().map(this::toUrl).toArray(URL[]::new);
assertThat(urls).containsExactlyInAnyOrder(expectedUrls);
}
@Test
void getClassPathUrlsWhenHasIncludeFilterReturnsUrls() throws Exception {
Set<URL> urls = this.archive.getClassPathUrls(this::entryNameIsNestedJar);
assertThat(urls).containsOnly(toUrl("nested.jar"));
}
@Test
void getClassPathUrlsWhenHasIncludeFilterAndSpaceInRootNameReturnsUrls() throws Exception {
createArchive("spaces in the name");
Set<URL> urls = this.archive.getClassPathUrls(this::entryNameIsNestedJar);
assertThat(urls).containsOnly(toUrl("nested.jar"));
}
@Test
void getClassPathUrlsWhenHasSearchFilterReturnsUrls() throws Exception {
Set<URL> urls = this.archive.getClassPathUrls(Archive.ALL_ENTRIES, (entry) -> !entry.name().equals("d/"));
assertThat(urls).contains(toUrl("nested.jar")).doesNotContain(toUrl("d/9.dat"));
}
private void createArchive() throws Exception {
createArchive(null);
}
private void createArchive(String directoryName) throws Exception {
File file = new File(this.tempDir, "test.jar");
TestJar.create(file);
this.rootDirectory = (StringUtils.hasText(directoryName) ? new File(this.tempDir, directoryName)
: new File(this.tempDir, UUID.randomUUID().toString()));
try (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 {
try (InputStream in = jarFile.getInputStream(entry);
OutputStream out = new FileOutputStream(destination)) {
in.transferTo(out);
}
}
}
this.archive = new ExplodedArchive(this.rootDirectory);
}
}
private URL toUrl(String name) {
return toUrl(new File(this.rootDirectory, name));
}
private URL toUrl(File file) {
try {
return file.toURI().toURL();
}
catch (MalformedURLException ex) {
throw new IllegalStateException(ex);
}
}
private boolean entryNameIsNestedJar(Entry entry) {
return entry.name().equals("nested.jar");
}
}

View File

@@ -0,0 +1,172 @@
/*
* 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.File;
import java.io.IOException;
import java.net.URL;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
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.launch.Archive.Entry;
import org.springframework.boot.loader.net.protocol.jar.JarUrl;
import org.springframework.boot.loader.testsupport.TestJar;
import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed;
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
*/
@AssertFileChannelDataBlocksClosed
class JarFileArchiveTests {
@TempDir
File tempDir;
private File file;
private JarFileArchive archive;
@BeforeEach
void setup() throws Exception {
createTestJarArchive(false);
}
@AfterEach
void tearDown() throws Exception {
this.archive.close();
}
@Test
void isExplodedReturnsFalse() {
assertThat(this.archive.isExploded()).isFalse();
}
@Test
void getRootDirectoryReturnsNull() {
assertThat(this.archive.getRootDirectory()).isNull();
}
@Test
void getManifestReturnsManifest() throws Exception {
assertThat(this.archive.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1");
}
@Test
void getClassPathUrlsWhenNoPredicartesReturnsUrls() throws Exception {
Set<URL> urls = this.archive.getClassPathUrls(Archive.ALL_ENTRIES);
URL[] expected = TestJar.expectedEntries()
.stream()
.map((name) -> JarUrl.create(this.file, name))
.toArray(URL[]::new);
assertThat(urls).containsExactly(expected);
}
@Test
void getClassPathUrlsWhenHasIncludeFilterReturnsUrls() throws Exception {
Set<URL> urls = this.archive.getClassPathUrls(this::entryNameIsNestedJar);
assertThat(urls).containsOnly(JarUrl.create(this.file, "nested.jar"));
}
@Test
void getClassPathUrlsWhenHasSearchFilterAllUrlsSinceSearchFilterIsNotUsed() throws Exception {
Set<URL> urls = this.archive.getClassPathUrls(Archive.ALL_ENTRIES, (entry) -> false);
URL[] expected = TestJar.expectedEntries()
.stream()
.map((name) -> JarUrl.create(this.file, name))
.toArray(URL[]::new);
assertThat(urls).containsExactly(expected);
}
@Test
void getClassPathUrlsWhenHasUnpackCommentUnpacksAndReturnsUrls() throws Exception {
createTestJarArchive(true);
Set<URL> urls = this.archive.getClassPathUrls(this::entryNameIsNestedJar);
assertThat(urls).hasSize(1);
URL url = urls.iterator().next();
assertThat(url).isNotEqualTo(JarUrl.create(this.file, "nested.jar"));
assertThat(url.toString()).startsWith("jar:file:").endsWith("/nested.jar!/");
}
@Test
void getClassPathUrlsWhenHasUnpackCommentUnpacksToUniqueLocationsPerArchive() throws Exception {
createTestJarArchive(true);
URL firstNestedUrl = this.archive.getClassPathUrls(this::entryNameIsNestedJar).iterator().next();
createTestJarArchive(true);
URL secondNestedUrl = this.archive.getClassPathUrls(this::entryNameIsNestedJar).iterator().next();
assertThat(secondNestedUrl).isNotEqualTo(firstNestedUrl);
}
@Test
void getClassPathUrlsWhenHasUnpackCommentUnpacksAndShareSameParent() throws Exception {
createTestJarArchive(true);
URL nestedUrl = this.archive.getClassPathUrls(this::entryNameIsNestedJar).iterator().next();
URL anotherNestedUrl = this.archive.getClassPathUrls((entry) -> entry.name().equals("another-nested.jar"))
.iterator()
.next();
assertThat(nestedUrl.toString())
.isEqualTo(anotherNestedUrl.toString().replace("another-nested.jar", "nested.jar"));
}
@Test
void getClassPathUrlsWhenZip64ListsAllEntries() throws Exception {
File file = new File(this.tempDir, "test.jar");
FileCopyUtils.copy(writeZip64Jar(), file);
try (Archive jarArchive = new JarFileArchive(file)) {
Set<URL> urls = jarArchive.getClassPathUrls(Archive.ALL_ENTRIES);
assertThat(urls).hasSize(65537);
}
}
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 void createTestJarArchive(boolean unpackNested) throws Exception {
if (this.archive != null) {
this.archive.close();
}
this.file = new File(this.tempDir, "root.jar");
TestJar.create(this.file, unpackNested);
this.archive = new JarFileArchive(this.file);
}
private boolean entryNameIsNestedJar(Entry entry) {
return entry.name().equals("nested.jar");
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.loader;
package org.springframework.boot.loader.launch;
import java.io.File;
import java.io.FileOutputStream;
@@ -23,17 +23,16 @@ import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.Manifest;
import org.junit.jupiter.api.Test;
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.net.protocol.jar.JarUrl;
import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.test.tools.SourceFile;
import org.springframework.core.test.tools.TestCompiler;
@@ -47,19 +46,17 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Andy Wilkinson
* @author Madhura Bhave
* @author Phillip Webb
*/
@AssertFileChannelDataBlocksClosed
class JarLauncherTests extends AbstractExecutableArchiveLauncherTests {
@Test
void explodedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath() throws Exception {
File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF"));
JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true));
List<Archive> archives = new ArrayList<>();
launcher.getClassPathArchivesIterator().forEachRemaining(archives::add);
assertThat(getUrls(archives)).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot));
for (Archive archive : archives) {
archive.close();
}
JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot));
Set<URL> urls = launcher.getClassPathUrls();
assertThat(urls).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot));
}
@Test
@@ -67,41 +64,33 @@ class JarLauncherTests extends AbstractExecutableArchiveLauncherTests {
File jarRoot = createJarArchive("archive.jar", "BOOT-INF");
try (JarFileArchive archive = new JarFileArchive(jarRoot)) {
JarLauncher launcher = new JarLauncher(archive);
List<Archive> classPathArchives = new ArrayList<>();
launcher.getClassPathArchivesIterator().forEachRemaining(classPathArchives::add);
assertThat(classPathArchives).hasSize(4);
assertThat(getUrls(classPathArchives)).containsOnly(
new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/classes!/"),
new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/foo.jar!/"),
new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/bar.jar!/"),
new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/baz.jar!/"));
for (Archive classPathArchive : classPathArchives) {
classPathArchive.close();
}
Set<URL> urls = launcher.getClassPathUrls();
List<URL> expectedUrls = new ArrayList<>();
expectedUrls.add(JarUrl.create(jarRoot, "BOOT-INF/classes/"));
expectedUrls.add(JarUrl.create(jarRoot, "BOOT-INF/lib/foo.jar"));
expectedUrls.add(JarUrl.create(jarRoot, "BOOT-INF/lib/bar.jar"));
expectedUrls.add(JarUrl.create(jarRoot, "BOOT-INF/lib/baz.jar"));
assertThat(urls).containsOnlyOnceElementsOf(expectedUrls);
}
}
@Test
void explodedJarShouldPreserveClasspathOrderWhenIndexPresent() throws Exception {
File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF", true, Collections.emptyList()));
JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true));
Iterator<Archive> archives = launcher.getClassPathArchivesIterator();
URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives);
URL[] urls = classLoader.getURLs();
assertThat(urls).containsExactly(getExpectedFileUrls(explodedRoot));
JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot));
URLClassLoader classLoader = createClassLoader(launcher);
assertThat(classLoader.getURLs()).containsExactly(getExpectedFileUrls(explodedRoot));
}
@Test
void jarFilesPresentInBootInfLibsAndNotInClasspathIndexShouldBeAddedAfterBootInfClasses() throws Exception {
ArrayList<String> extraLibs = new ArrayList<>(Arrays.asList("extra-1.jar", "extra-2.jar"));
File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF", true, extraLibs));
JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true));
Iterator<Archive> archives = launcher.getClassPathArchivesIterator();
URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives);
URL[] urls = classLoader.getURLs();
JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot));
URLClassLoader classLoader = createClassLoader(launcher);
List<File> expectedFiles = getExpectedFilesWithExtraLibs(explodedRoot);
URL[] expectedFileUrls = expectedFiles.stream().map(this::toUrl).toArray(URL[]::new);
assertThat(urls).containsExactly(expectedFileUrls);
assertThat(classLoader.getURLs()).containsExactly(expectedFileUrls);
}
@Test
@@ -119,19 +108,22 @@ class JarLauncherTests extends AbstractExecutableArchiveLauncherTests {
target.getParentFile().mkdirs();
FileCopyUtils.copy(compiled.getClassLoader().getResourceAsStream("explodedsample/ExampleClass.class"),
new FileOutputStream(target));
JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true));
Iterator<Archive> archives = launcher.getClassPathArchivesIterator();
URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives);
JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot));
URLClassLoader classLoader = createClassLoader(launcher);
Class<?> loaded = classLoader.loadClass("explodedsample.ExampleClass");
assertThat(loaded.getPackage().getImplementationTitle()).isEqualTo("test");
}));
}
protected final URL[] getExpectedFileUrls(File explodedRoot) {
private URLClassLoader createClassLoader(JarLauncher launcher) throws Exception {
return (URLClassLoader) launcher.createClassLoader(launcher.getClassPathUrls());
}
private URL[] getExpectedFileUrls(File explodedRoot) {
return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new);
}
protected final List<File> getExpectedFiles(File parent) {
private List<File> getExpectedFiles(File parent) {
List<File> expected = new ArrayList<>();
expected.add(new File(parent, "BOOT-INF/classes"));
expected.add(new File(parent, "BOOT-INF/lib/foo.jar"));
@@ -140,7 +132,7 @@ class JarLauncherTests extends AbstractExecutableArchiveLauncherTests {
return expected;
}
protected final List<File> getExpectedFilesWithExtraLibs(File parent) {
private List<File> getExpectedFilesWithExtraLibs(File parent) {
List<File> expected = new ArrayList<>();
expected.add(new File(parent, "BOOT-INF/classes"));
expected.add(new File(parent, "BOOT-INF/lib/extra-1.jar"));

View File

@@ -0,0 +1,49 @@
/*
* 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.net.URL;
import org.junit.jupiter.api.Test;
import org.springframework.boot.loader.jarmode.JarMode;
import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link LaunchedClassLoader}.
*
* @author Dave Syer
* @author Phillip Webb
* @author Andy Wilkinson
*/
@AssertFileChannelDataBlocksClosed
class LaunchedClassLoaderTests {
@Test
void loadClassWhenJarModeClassLoadsInLaunchedClassLoader() throws Exception {
try (LaunchedClassLoader classLoader = new LaunchedClassLoader(false, new URL[] {},
getClass().getClassLoader())) {
Class<?> jarModeClass = classLoader.loadClass(JarMode.class.getName());
Class<?> jarModeRunnerClass = classLoader.loadClass(JarModeRunner.class.getName());
assertThat(jarModeClass.getClassLoader()).isSameAs(classLoader);
assertThat(jarModeRunnerClass.getClassLoader()).isSameAs(classLoader);
}
}
}

View File

@@ -14,54 +14,64 @@
* limitations under the License.
*/
package org.springframework.boot.loader.jarmode;
package org.springframework.boot.loader.launch;
import java.net.URL;
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.loader.Launcher;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed;
import org.springframework.boot.testsupport.system.CapturedOutput;
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Launcher} with jar mode support.
* Tests for {@link Launcher}.
*
* @author Phillip Webb
*/
@ExtendWith(OutputCaptureExtension.class)
class LauncherJarModeTests {
@AssertFileChannelDataBlocksClosed
class LauncherTests {
@BeforeEach
void setup() {
System.setProperty(JarModeLauncher.DISABLE_SYSTEM_EXIT, "true");
}
/**
* Jar Mode tests.
*/
@Nested
class JarMode {
@AfterEach
void cleanup() {
System.clearProperty("jarmode");
System.clearProperty(JarModeLauncher.DISABLE_SYSTEM_EXIT);
}
@BeforeEach
void setup() {
System.setProperty(JarModeRunner.DISABLE_SYSTEM_EXIT, "true");
}
@Test
void launchWhenJarModePropertyIsSetLaunchesJarMode(CapturedOutput out) throws Exception {
System.setProperty("jarmode", "test");
new TestLauncher().launch(new String[] { "boot" });
assertThat(out).contains("running in test jar mode [boot]");
}
@AfterEach
void cleanup() {
System.clearProperty("jarmode");
System.clearProperty(JarModeRunner.DISABLE_SYSTEM_EXIT);
}
@Test
void launchWhenJarModePropertyIsSetLaunchesJarMode(CapturedOutput out) throws Exception {
System.setProperty("jarmode", "test");
new TestLauncher().launch(new String[] { "boot" });
assertThat(out).contains("running in test jar mode [boot]");
}
@Test
void launchWhenJarModePropertyIsNotAcceptedThrowsException(CapturedOutput out) throws Exception {
System.setProperty("jarmode", "idontexist");
new TestLauncher().launch(new String[] { "boot" });
assertThat(out).contains("Unsupported jarmode 'idontexist'");
}
@Test
void launchWhenJarModePropertyIsNotAcceptedThrowsException(CapturedOutput out) throws Exception {
System.setProperty("jarmode", "idontexist");
new TestLauncher().launch(new String[] { "boot" });
assertThat(out).contains("Unsupported jarmode 'idontexist'");
}
private static class TestLauncher extends Launcher {
@@ -72,8 +82,13 @@ class LauncherJarModeTests {
}
@Override
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
return Collections.emptyIterator();
protected Archive getArchive() {
return null;
}
@Override
protected Set<URL> getClassPathUrls() throws Exception {
return Collections.emptySet();
}
@Override

View File

@@ -14,20 +14,17 @@
* limitations under the License.
*/
package org.springframework.boot.loader;
package org.springframework.boot.loader.launch;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.net.URL;
import java.net.URLClassLoader;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
@@ -39,11 +36,9 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
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.Handler;
import org.springframework.boot.loader.jar.JarFile;
import org.springframework.boot.loader.net.protocol.jar.JarUrl;
import org.springframework.boot.loader.testsupport.TestJar;
import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed;
import org.springframework.boot.testsupport.system.CapturedOutput;
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
import org.springframework.core.io.FileSystemResource;
@@ -51,7 +46,7 @@ import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.hamcrest.Matchers.containsString;
/**
@@ -61,6 +56,7 @@ import static org.hamcrest.Matchers.containsString;
* @author Andy Wilkinson
*/
@ExtendWith(OutputCaptureExtension.class)
@AssertFileChannelDataBlocksClosed
class PropertiesLauncherTests {
@TempDir
@@ -73,9 +69,8 @@ class PropertiesLauncherTests {
private CapturedOutput output;
@BeforeEach
void setup(CapturedOutput capturedOutput) throws Exception {
void setup(CapturedOutput capturedOutput) {
this.contextClassLoader = Thread.currentThread().getContextClassLoader();
clearHandlerCache();
System.setProperty("loader.home", new File("src/test/resources").getAbsolutePath());
this.output = capturedOutput;
}
@@ -90,26 +85,13 @@ class PropertiesLauncherTests {
System.clearProperty("loader.config.location");
System.clearProperty("loader.system");
System.clearProperty("loader.classLoader");
clearHandlerCache();
if (this.launcher != null) {
this.launcher.close();
}
}
@SuppressWarnings("unchecked")
private void clearHandlerCache() throws Exception {
Map<File, JarFile> rootFileCache = ((SoftReference<Map<File, JarFile>>) ReflectionTestUtils
.getField(Handler.class, "rootFileCache")).get();
if (rootFileCache != null) {
for (JarFile rootJarFile : rootFileCache.values()) {
rootJarFile.close();
}
rootFileCache.clear();
}
}
@Test
void testDefaultHome() {
void testDefaultHome() throws Exception {
System.clearProperty("loader.home");
this.launcher = new PropertiesLauncher();
assertThat(this.launcher.getHomeDirectory()).isEqualTo(new File(System.getProperty("user.dir")));
@@ -126,9 +108,8 @@ class PropertiesLauncherTests {
@Test
void testNonExistentHome() {
System.setProperty("loader.home", "src/test/resources/nonexistent");
assertThatIllegalStateException().isThrownBy(PropertiesLauncher::new)
.withMessageContaining("Invalid source directory")
.withCauseInstanceOf(IllegalArgumentException.class);
assertThatIllegalArgumentException().isThrownBy(PropertiesLauncher::new)
.withMessageContaining("Invalid source directory");
}
@Test
@@ -154,7 +135,7 @@ class PropertiesLauncherTests {
}
@Test
void testUserSpecifiedDotPath() {
void testUserSpecifiedDotPath() throws Exception {
System.setProperty("loader.path", ".");
this.launcher = new PropertiesLauncher();
assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[.]");
@@ -165,9 +146,8 @@ class PropertiesLauncherTests {
System.setProperty("loader.path", "jars/");
this.launcher = new PropertiesLauncher();
assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[jars/]");
List<Archive> archives = new ArrayList<>();
this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add);
assertThat(archives).areExactly(1, endingWith("app.jar"));
Set<URL> urls = this.launcher.getClassPathUrls();
assertThat(urls).areExactly(1, endingWith("app.jar"));
}
@Test
@@ -196,29 +176,26 @@ class PropertiesLauncherTests {
this.launcher = new PropertiesLauncher();
assertThat(ReflectionTestUtils.getField(this.launcher, "paths"))
.hasToString("[jar:file:./src/test/resources/nested-jars/app.jar!/]");
List<Archive> archives = new ArrayList<>();
this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add);
assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
assertThat(archives).areExactly(1, endingWith("app.jar"));
Set<URL> urls = this.launcher.getClassPathUrls();
assertThat(urls).areExactly(1, endingWith("foo.jar!/"));
assertThat(urls).areExactly(1, endingWith("app.jar!/"));
}
@Test
void testUserSpecifiedRootOfJarPathWithDot() throws Exception {
System.setProperty("loader.path", "nested-jars/app.jar!/./");
this.launcher = new PropertiesLauncher();
List<Archive> archives = new ArrayList<>();
this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add);
assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
assertThat(archives).areExactly(1, endingWith("app.jar"));
Set<URL> urls = this.launcher.getClassPathUrls();
assertThat(urls).areExactly(1, endingWith("foo.jar!/"));
assertThat(urls).areExactly(1, endingWith("app.jar!/"));
}
@Test
void testUserSpecifiedRootOfJarPathWithDotAndJarPrefix() throws Exception {
System.setProperty("loader.path", "jar:file:./src/test/resources/nested-jars/app.jar!/./");
this.launcher = new PropertiesLauncher();
List<Archive> archives = new ArrayList<>();
this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add);
assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
Set<URL> urls = this.launcher.getClassPathUrls();
assertThat(urls).areExactly(1, endingWith("foo.jar!/"));
}
@Test
@@ -226,10 +203,9 @@ class PropertiesLauncherTests {
System.setProperty("loader.path", "nested-jars/app.jar");
System.setProperty("loader.main", "demo.Application");
this.launcher = new PropertiesLauncher();
List<Archive> archives = new ArrayList<>();
this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add);
assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
assertThat(archives).areExactly(1, endingWith("app.jar"));
Set<URL> urls = this.launcher.getClassPathUrls();
assertThat(urls).areExactly(1, endingWith("foo.jar!/"));
assertThat(urls).areExactly(1, endingWith("app.jar"));
}
@Test
@@ -287,32 +263,21 @@ class PropertiesLauncherTests {
void testCustomClassLoaderCreation() throws Exception {
System.setProperty("loader.classLoader", TestLoader.class.getName());
this.launcher = new PropertiesLauncher();
ClassLoader loader = this.launcher.createClassLoader(archives());
ClassLoader loader = this.launcher.createClassLoader(classPathUrls());
assertThat(loader).isNotNull();
assertThat(loader.getClass().getName()).isEqualTo(TestLoader.class.getName());
}
private Iterator<Archive> archives() throws Exception {
List<Archive> archives = new ArrayList<>();
String path = System.getProperty("java.class.path");
for (String url : path.split(File.pathSeparator)) {
Archive archive = archive(url);
if (archive != null) {
archives.add(archive);
private Set<URL> classPathUrls() throws Exception {
Set<URL> urls = new LinkedHashSet<>();
String classPath = System.getProperty("java.class.path");
for (String path : classPath.split(File.pathSeparator)) {
File file = new FileSystemResource(path).getFile();
if (file.exists()) {
urls.add(file.toURI().toURL());
}
}
return archives.iterator();
}
private Archive archive(String url) throws IOException {
File file = new FileSystemResource(url).getFile();
if (!file.exists()) {
return null;
}
if (url.endsWith(".jar")) {
return new JarFileArchive(file);
}
return new ExplodedArchive(file);
return urls;
}
@Test
@@ -331,7 +296,7 @@ class PropertiesLauncherTests {
}
@Test
void testSystemPropertiesSet() {
void testSystemPropertiesSet() throws Exception {
System.setProperty("loader.system", "true");
new PropertiesLauncher();
assertThat(System.getProperty("loader.main")).isEqualTo("demo.Application");
@@ -374,17 +339,15 @@ class PropertiesLauncherTests {
loaderPath.mkdir();
System.setProperty("loader.path", loaderPath.toURI().toURL().toString());
this.launcher = new PropertiesLauncher();
List<Archive> archives = new ArrayList<>();
this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add);
assertThat(archives).hasSize(1);
File archiveRoot = (File) ReflectionTestUtils.getField(archives.get(0), "root");
assertThat(archiveRoot).isEqualTo(loaderPath);
Set<URL> urls = this.launcher.getClassPathUrls();
assertThat(urls).hasSize(1);
assertThat(urls.iterator().next()).isEqualTo(loaderPath.toURI().toURL());
}
@Test // gh-21575
void loadResourceFromJarFile() throws Exception {
File jarFile = new File(this.tempDir, "app.jar");
TestJarCreator.createTestJar(jarFile);
File file = new File(this.tempDir, "app.jar");
TestJar.create(file);
System.setProperty("loader.home", this.tempDir.getAbsolutePath());
System.setProperty("loader.path", "app.jar");
this.launcher = new PropertiesLauncher();
@@ -393,11 +356,10 @@ class PropertiesLauncherTests {
}
catch (Exception ex) {
// Expected ClassNotFoundException
LaunchedURLClassLoader classLoader = (LaunchedURLClassLoader) Thread.currentThread()
.getContextClassLoader();
LaunchedClassLoader classLoader = (LaunchedClassLoader) Thread.currentThread().getContextClassLoader();
classLoader.close();
}
URL resource = new URL("jar:" + jarFile.toURI() + "!/nested.jar!/3.dat");
URL resource = JarUrl.create(file, "nested.jar", "3.dat");
byte[] bytes = FileCopyUtils.copyToByteArray(resource.openStream());
assertThat(bytes).isNotEmpty();
}
@@ -406,11 +368,11 @@ class PropertiesLauncherTests {
Awaitility.waitAtMost(Duration.ofSeconds(5)).until(this.output::toString, containsString(value));
}
private Condition<Archive> endingWith(String value) {
private Condition<URL> endingWith(String value) {
return new Condition<>() {
@Override
public boolean matches(Archive archive) {
public boolean matches(URL archive) {
return archive.toString().endsWith(value);
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.loader;
package org.springframework.boot.loader.launch;
import java.io.File;
import java.net.URL;
@@ -22,14 +22,13 @@ import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
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.net.protocol.jar.JarUrl;
import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed;
import static org.assertj.core.api.Assertions.assertThat;
@@ -38,45 +37,39 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Andy Wilkinson
* @author Scott Frederick
* @author Phillip Webb
*/
@AssertFileChannelDataBlocksClosed
class WarLauncherTests extends AbstractExecutableArchiveLauncherTests {
@Test
void explodedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath() throws Exception {
File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF"));
WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true));
List<Archive> archives = new ArrayList<>();
launcher.getClassPathArchivesIterator().forEachRemaining(archives::add);
assertThat(getUrls(archives)).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot));
for (Archive archive : archives) {
archive.close();
}
WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot));
Set<URL> urls = launcher.getClassPathUrls();
assertThat(urls).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot));
}
@Test
void archivedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath() throws Exception {
File jarRoot = createJarArchive("archive.war", "WEB-INF");
try (JarFileArchive archive = new JarFileArchive(jarRoot)) {
File file = createJarArchive("archive.war", "WEB-INF");
try (JarFileArchive archive = new JarFileArchive(file)) {
WarLauncher launcher = new WarLauncher(archive);
List<Archive> classPathArchives = new ArrayList<>();
launcher.getClassPathArchivesIterator().forEachRemaining(classPathArchives::add);
assertThat(getUrls(classPathArchives)).containsOnly(
new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/classes!/"),
new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/foo.jar!/"),
new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/bar.jar!/"),
new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/baz.jar!/"));
for (Archive classPathArchive : classPathArchives) {
classPathArchive.close();
}
Set<URL> urls = launcher.getClassPathUrls();
List<URL> expected = new ArrayList<>();
expected.add(JarUrl.create(file, "WEB-INF/classes/"));
expected.add(JarUrl.create(file, "WEB-INF/lib/foo.jar"));
expected.add(JarUrl.create(file, "WEB-INF/lib/bar.jar"));
expected.add(JarUrl.create(file, "WEB-INF/lib/baz.jar"));
assertThat(urls).containsOnly(expected.toArray(URL[]::new));
}
}
@Test
void explodedWarShouldPreserveClasspathOrderWhenIndexPresent() throws Exception {
File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF", true, Collections.emptyList()));
WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true));
Iterator<Archive> archives = launcher.getClassPathArchivesIterator();
URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives);
WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot));
URLClassLoader classLoader = createClassLoader(launcher);
URL[] urls = classLoader.getURLs();
assertThat(urls).containsExactly(getExpectedFileUrls(explodedRoot));
}
@@ -85,20 +78,23 @@ class WarLauncherTests extends AbstractExecutableArchiveLauncherTests {
void warFilesPresentInWebInfLibsAndNotInClasspathIndexShouldBeAddedAfterWebInfClasses() throws Exception {
ArrayList<String> extraLibs = new ArrayList<>(Arrays.asList("extra-1.jar", "extra-2.jar"));
File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF", true, extraLibs));
WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true));
Iterator<Archive> archives = launcher.getClassPathArchivesIterator();
URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives);
WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot));
URLClassLoader classLoader = createClassLoader(launcher);
URL[] urls = classLoader.getURLs();
List<File> expectedFiles = getExpectedFilesWithExtraLibs(explodedRoot);
URL[] expectedFileUrls = expectedFiles.stream().map(this::toUrl).toArray(URL[]::new);
assertThat(urls).containsExactly(expectedFileUrls);
}
protected final URL[] getExpectedFileUrls(File explodedRoot) {
private URLClassLoader createClassLoader(Launcher launcher) throws Exception {
return (URLClassLoader) launcher.createClassLoader(launcher.getClassPathUrls());
}
private URL[] getExpectedFileUrls(File explodedRoot) {
return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new);
}
protected final List<File> getExpectedFiles(File parent) {
private List<File> getExpectedFiles(File parent) {
List<File> expected = new ArrayList<>();
expected.add(new File(parent, "WEB-INF/classes"));
expected.add(new File(parent, "WEB-INF/lib/foo.jar"));
@@ -107,7 +103,7 @@ class WarLauncherTests extends AbstractExecutableArchiveLauncherTests {
return expected;
}
protected final List<File> getExpectedFilesWithExtraLibs(File parent) {
private List<File> getExpectedFilesWithExtraLibs(File parent) {
List<File> expected = new ArrayList<>();
expected.add(new File(parent, "WEB-INF/classes"));
expected.add(new File(parent, "WEB-INF/lib/extra-1.jar"));

View File

@@ -0,0 +1,57 @@
/*
* 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 org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Canonicalizer}.
*
* @author Phillip Webb
*/
class CanonicalizerTests {
@Test
void canonicalizeAfterOnlyChangesAfterPos() {
String prefix = "/foo/.././bar/.!/foo/.././bar/.";
String canonicalized = Canonicalizer.canonicalizeAfter(prefix, prefix.indexOf("!/"));
assertThat(canonicalized).isEqualTo("/foo/.././bar/.!/bar/");
}
@Test
void canonicalizeWhenHasEmbdeddSlashDotDotSlash() {
assertThat(Canonicalizer.canonicalize("/foo/../bar/bif/bam/../../baz")).isEqualTo("/bar/baz");
}
@Test
void canonicalizeWhenHasEmbdeddSlashDotSlash() {
assertThat(Canonicalizer.canonicalize("/foo/./bar/bif/bam/././baz")).isEqualTo("/foo/bar/bif/bam/baz");
}
@Test
void canonicalizeWhenHasTrailingSlashDotDot() {
assertThat(Canonicalizer.canonicalize("/foo/bar/baz/../..")).isEqualTo("/foo/");
}
@Test
void canonicalizeWhenHasTrailingSlashDot() {
assertThat(Canonicalizer.canonicalize("/foo/bar/baz/./.")).isEqualTo("/foo/bar/baz/");
}
}

View File

@@ -0,0 +1,203 @@
/*
* 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.net.MalformedURLException;
import java.net.URL;
import org.junit.jupiter.api.Test;
import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link Handler}.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
@AssertFileChannelDataBlocksClosed
class HandlerTests {
private final Handler handler = new Handler();
@Test
void indexOfSeparator() {
String spec = "jar:nested:foo!bar!/some/entry#foo";
assertThat(Handler.indexOfSeparator(spec, 0, spec.indexOf('#'))).isEqualTo(spec.lastIndexOf("!/"));
}
@Test
void indexOfSeparatorWhenHasStartAndLimit() {
String spec = "a!/jar:nested:foo!bar!/some/entry#foo!/b";
int beginIndex = 3;
int endIndex = spec.length() - 4;
String substring = spec.substring(beginIndex, endIndex);
assertThat(Handler.indexOfSeparator(spec, 0, spec.indexOf('#')))
.isEqualTo(substring.lastIndexOf("!/") + beginIndex);
}
@Test
void parseUrlWhenAbsoluteParses() throws MalformedURLException {
URL url = createJarUrl("");
String spec = "jar:file:example.jar!/entry.txt";
this.handler.parseURL(url, spec, 4, spec.length());
assertThat(url.toExternalForm()).isEqualTo(spec);
}
@Test
void parseUrlWhenAbsoluteWithAnchorParses() throws MalformedURLException {
URL url = createJarUrl("");
String spec = "jar:file:example.jar!/entry.txt";
this.handler.parseURL(url, spec + "#foo", 4, spec.length());
assertThat(url.toExternalForm()).isEqualTo(spec + "#foo");
}
@Test
void parseUrlWhenAbsoluteWithNoSeparatorThrowsException() throws MalformedURLException {
URL url = createJarUrl("");
String spec = "jar:file:example.jar!\\entry.txt";
assertThatIllegalStateException().isThrownBy(() -> this.handler.parseURL(url, spec, 4, spec.length()))
.withMessage("no !/ in spec");
}
@Test
void parseUrlWhenAbsoluteWithMalformedInnerUrlThrowsException() throws MalformedURLException {
URL url = createJarUrl("");
String spec = "jar:example.jar!/entry.txt";
assertThatIllegalStateException().isThrownBy(() -> this.handler.parseURL(url, spec, 4, spec.length()))
.withMessage(
"invalid url: jar:example.jar!/entry.txt (java.net.MalformedURLException: no protocol: example.jar)");
}
@Test
void parseUrlWhenRelativeWithLeadingSlashParses() throws MalformedURLException {
URL url = createJarUrl("file:example.jar!/entry.txt");
String spec = "/other.txt";
this.handler.parseURL(url, spec, 0, spec.length());
assertThat(url.toExternalForm()).isEqualTo("jar:file:example.jar!/other.txt");
}
@Test
void parseUrlWhenRelativeWithLeadingSlashAndAnchorParses() throws MalformedURLException {
URL url = createJarUrl("file:example.jar!/entry.txt");
String spec = "/other.txt";
this.handler.parseURL(url, spec + "#relative", 0, spec.length());
assertThat(url.toExternalForm()).isEqualTo("jar:file:example.jar!/other.txt#relative");
}
@Test
void parseUrlWhenRelativeWithLeadingSlashAndNoSeparator() throws MalformedURLException {
URL url = createJarUrl("file:example.jar/entry.txt");
String spec = "/other.txt";
assertThatIllegalStateException().isThrownBy(() -> this.handler.parseURL(url, spec, 0, spec.length()))
.withMessage("malformed context url:jar:file:example.jar/entry.txt: no !/");
}
@Test
void parseUrlWhenRelativeWithoutLeadingSlashParses() throws MalformedURLException {
URL url = createJarUrl("file:example.jar!/foo/");
String spec = "bar.txt";
this.handler.parseURL(url, spec, 0, spec.length());
assertThat(url.toExternalForm()).isEqualTo("jar:file:example.jar!/foo/bar.txt");
}
@Test
void parseUrlWhenRelativeWithoutLeadingSlashAndWithoutTrailingSlashParses() throws MalformedURLException {
URL url = createJarUrl("file:example.jar!/foo/baz");
String spec = "bar.txt";
this.handler.parseURL(url, spec, 0, spec.length());
assertThat(url.toExternalForm()).isEqualTo("jar:file:example.jar!/foo/bar.txt");
}
@Test
void parseUrlWhenRelativeWithoutLeadingSlashAndWithoutContextSlashThrowsException() throws MalformedURLException {
URL url = createJarUrl("file:example.jar");
String spec = "bar.txt";
assertThatIllegalStateException().isThrownBy(() -> this.handler.parseURL(url, spec, 0, spec.length()))
.withMessage("malformed context url:jar:file:example.jar");
}
@Test
void parseUrlWhenAnchorOnly() throws MalformedURLException {
URL url = createJarUrl("file:example.jar!/entry.txt");
String spec = "#runtime";
this.handler.parseURL(url, spec, 0, 0);
assertThat(url.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt#runtime");
}
@Test
void hashCodeGeneratesHashCode() throws MalformedURLException {
URL url = createJarUrl("file:example.jar!/entry.txt");
assertThat(this.handler.hashCode(url)).isEqualTo(1873709601);
}
@Test
void hashCodeWhenMalformedInnerUrlGeneratesHashCode() throws MalformedURLException {
URL url = createJarUrl("example.jar!/entry.txt");
assertThat(this.handler.hashCode(url)).isEqualTo(1870566566);
}
@Test
void sameFileWhenSameReturnsTrue() throws MalformedURLException {
URL url1 = createJarUrl("file:example.jar!/entry.txt");
URL url2 = createJarUrl("file:example.jar!/entry.txt");
assertThat(this.handler.sameFile(url1, url2)).isTrue();
}
@Test
void sameFileWhenMissingSeparatorReturnsFalse() throws MalformedURLException {
URL url1 = createJarUrl("file:example.jar!/entry.txt");
URL url2 = createJarUrl("file:example.jar/entry.txt");
assertThat(this.handler.sameFile(url1, url2)).isFalse();
}
@Test
void sameFileWhenDifferentEntryReturnsFalse() throws MalformedURLException {
URL url1 = createJarUrl("file:example.jar!/entry1.txt");
URL url2 = createJarUrl("file:example.jar!/entry2.txt");
assertThat(this.handler.sameFile(url1, url2)).isFalse();
}
@Test
void sameFileWhenDifferentInnerUrlReturnsFalse() throws MalformedURLException {
URL url1 = createJarUrl("file:example1.jar!/entry.txt");
URL url2 = createJarUrl("file:example2.jar!/entry.txt");
assertThat(this.handler.sameFile(url1, url2)).isFalse();
}
@Test
void sameFileWhenSameMalformedInnerUrlReturnsTrue() throws MalformedURLException {
URL url1 = createJarUrl("example.jar!/entry.txt");
URL url2 = createJarUrl("example.jar!/entry.txt");
assertThat(this.handler.sameFile(url1, url2)).isTrue();
}
@Test
void sameFileWhenDifferentMalformedInnerUrlReturnsFalse() throws MalformedURLException {
URL url1 = createJarUrl("example1.jar!/entry.txt");
URL url2 = createJarUrl("example2.jar!/entry.txt");
assertThat(this.handler.sameFile(url1, url2)).isFalse();
}
private URL createJarUrl(String file) throws MalformedURLException {
return new URL("jar", null, -1, file, this.handler);
}
}

View File

@@ -0,0 +1,88 @@
/*
* 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.net.URL;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.boot.loader.net.protocol.Handlers;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JarFileUrlKey}.
*
* @author Phillip Webb
*/
class JarFileUrlKeyTests {
@BeforeAll
static void setup() {
Handlers.register();
}
@Test
void getCreatesKey() throws Exception {
URL url = new URL("jar:nested:/my.jar/!mynested.jar!/my/path");
assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path");
}
@Test
void getWhenUppercaseProtocolCreatesKey() throws Exception {
URL url = new URL("JAR:nested:/my.jar/!mynested.jar!/my/path");
assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path");
}
@Test
void getWhenHasHostAndPortCreatesKey() throws Exception {
URL url = new URL("https://example.com:1234/test");
assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:1234/test");
}
@Test
void getWhenHasUppercaseHostCreatesKey() throws Exception {
URL url = new URL("https://EXAMPLE.com:1234/test");
assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:1234/test");
}
@Test
void getWhenHasNoPortCreatesKeyWithDefaultPort() throws Exception {
URL url = new URL("https://EXAMPLE.com/test");
assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:443/test");
}
@Test
void getWhenHasNoFileCreatesKey() throws Exception {
URL url = new URL("https://EXAMPLE.com");
assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:443");
}
@Test
void getWhenHasRuntimeRefCreatesKey() throws Exception {
URL url = new URL("jar:nested:/my.jar/!mynested.jar!/my/path#runtime");
assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path#runtime");
}
@Test
void getWhenHasOtherRefCreatesKeyWithoutRef() throws Exception {
URL url = new URL("jar:nested:/my.jar/!mynested.jar!/my/path#example");
assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path");
}
}

View File

@@ -0,0 +1,147 @@
/*
* 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.FileOutputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
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.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.loader.net.protocol.Handlers;
import org.springframework.boot.loader.testsupport.TestJar;
import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JarUrlClassLoader}.
*
* @author Phillip Webb
*/
@AssertFileChannelDataBlocksClosed
class JarUrlClassLoaderTests {
private static final URL APP_JAR;
static {
try {
APP_JAR = new URL("jar:file:src/test/resources/jars/app.jar!/");
}
catch (MalformedURLException ex) {
throw new IllegalStateException(ex);
}
}
@TempDir
File tempDir;
@BeforeAll
static void setup() {
Handlers.register();
}
@Test
void resolveResourceFromArchive() throws Exception {
try (JarUrlClassLoader loader = new TestJarUrlClassLoader(APP_JAR)) {
assertThat(loader.getResource("demo/Application.java")).isNotNull();
}
}
@Test
void resolveResourcesFromArchive() throws Exception {
try (JarUrlClassLoader loader = new TestJarUrlClassLoader(APP_JAR)) {
assertThat(loader.getResources("demo/Application.java").hasMoreElements()).isTrue();
}
}
@Test
void resolveRootPathFromArchive() throws Exception {
try (JarUrlClassLoader loader = new TestJarUrlClassLoader(APP_JAR)) {
assertThat(loader.getResource("")).isNotNull();
}
}
@Test
void resolveRootResourcesFromArchive() throws Exception {
try (JarUrlClassLoader loader = new TestJarUrlClassLoader(APP_JAR)) {
assertThat(loader.getResources("").hasMoreElements()).isTrue();
}
}
@Test
void resolveFromNested() throws Exception {
File jarFile = new File(this.tempDir, "test.jar");
TestJar.create(jarFile);
URL url = JarUrl.create(jarFile, "nested.jar");
try (JarUrlClassLoader loader = new TestJarUrlClassLoader(url)) {
URL resource = loader.getResource("3.dat");
assertThat(resource).hasToString(url + "3.dat");
try (InputStream input = resource.openConnection().getInputStream()) {
assertThat(input.read()).isEqualTo(3);
}
}
}
@Test
void loadClass() throws Exception {
try (JarUrlClassLoader loader = new TestJarUrlClassLoader(APP_JAR)) {
assertThat(loader.loadClass("demo.Application")).isNotNull().hasToString("class demo.Application");
}
}
@Test
void loadClassFromNested() throws Exception {
File appJar = new File("src/test/resources/jars/app.jar");
File jarFile = new File(this.tempDir, "test.jar");
FileOutputStream fileOutputStream = new FileOutputStream(jarFile);
try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) {
JarEntry nestedEntry = new JarEntry("app.jar");
byte[] nestedJarData = Files.readAllBytes(appJar.toPath());
nestedEntry.setSize(nestedJarData.length);
nestedEntry.setCompressedSize(nestedJarData.length);
CRC32 crc32 = new CRC32();
crc32.update(nestedJarData);
nestedEntry.setCrc(crc32.getValue());
nestedEntry.setMethod(ZipEntry.STORED);
jarOutputStream.putNextEntry(nestedEntry);
jarOutputStream.write(nestedJarData);
jarOutputStream.closeEntry();
}
URL url = JarUrl.create(jarFile, "app.jar");
try (JarUrlClassLoader loader = new TestJarUrlClassLoader(url)) {
assertThat(loader.loadClass("demo.Application")).isNotNull().hasToString("class demo.Application");
}
}
static class TestJarUrlClassLoader extends JarUrlClassLoader {
TestJarUrlClassLoader(URL... urls) {
super(urls, JarUrlClassLoaderTests.class.getClassLoader());
}
}
}

View File

@@ -0,0 +1,480 @@
/*
* 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.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.security.Permission;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.loader.net.protocol.Handlers;
import org.springframework.boot.loader.testsupport.TestJar;
import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed;
import org.springframework.boot.loader.zip.ZipContent;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIOException;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
/**
* Tests for {@link JarUrlConnection}.
*
* @author Phillip Webb
*/
@AssertFileChannelDataBlocksClosed
class JarUrlConnectionTests {
@TempDir
File temp;
private File file;
private URL url;
@BeforeAll
static void registerHandlers() {
Handlers.register();
}
@BeforeEach
@AfterEach
void reset() {
JarUrlConnection.clearCache();
Optimizations.disable();
}
@BeforeEach
void setup() throws Exception {
this.file = new File(this.temp, "test.jar");
TestJar.create(this.file);
this.url = JarUrl.create(this.file, "nested.jar");
}
@Test
void getJarFileReturnsJarFile() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
JarFile jarFile = connection.getJarFile();
assertThat(jarFile).isNotNull();
assertThat(jarFile.getEntry("3.dat")).isNotNull();
}
@Test
void getJarEntryReturnsJarEntry() throws Exception {
URL url = JarUrl.create(this.file, "nested.jar", "3.dat");
JarUrlConnection connection = JarUrlConnection.open(url);
JarEntry entry = connection.getJarEntry();
assertThat(entry).isNotNull();
assertThat(entry.getName()).isEqualTo("3.dat");
}
@Test
void getJarEntryWhenHasNoEntryNameReturnsNull() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
JarEntry entry = connection.getJarEntry();
assertThat(entry).isNull();
}
@Test
void getContentLengthReturnsContentLength() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
try (ZipContent content = ZipContent.open(this.file.toPath())) {
int expected = content.getEntry("nested.jar").getUncompressedSize();
assertThat(connection.getContentLength()).isEqualTo(expected);
}
}
@Test
void getContentLengthWhenLengthIsLargerThanMaxIntReturnsMinusOne() {
JarUrlConnection connection = mock(JarUrlConnection.class);
given(connection.getContentLength()).willCallRealMethod();
given(connection.getContentLengthLong()).willReturn((long) Integer.MAX_VALUE + 1);
assertThat(connection.getContentLength()).isEqualTo(-1);
}
@Test
void getContentLengthLongWhenHasNoEntryReturnsSizeOfJar() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
try (ZipContent content = ZipContent.open(this.file.toPath())) {
int expected = content.getEntry("nested.jar").getUncompressedSize();
assertThat(connection.getContentLengthLong()).isEqualTo(expected);
}
}
@Test
void getContentLengthLongWhenHasEntryReturnsEntrySize() throws Exception {
URL url = JarUrl.create(this.file, "nested.jar", "3.dat");
JarUrlConnection connection = JarUrlConnection.open(url);
assertThat(connection.getContentLengthLong()).isEqualTo(1);
}
@Test
void getContentLengthLongWhenCannotConnectReturnsMinusOne() throws IOException {
JarUrlConnection connection = mock(JarUrlConnection.class);
willThrow(IOException.class).given(connection).connect();
given(connection.getContentLengthLong()).willCallRealMethod();
assertThat(connection.getContentLengthLong()).isEqualTo(-1);
}
@Test
void getContentTypeWhenHasNoEntryReturnsJavaJar() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
assertThat(connection.getContentType()).isEqualTo("x-java/jar");
}
@Test
void getContentTypeWhenHasKnownStreamReturnsDeducedType() throws Exception {
String content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><ok></ok>";
try (JarOutputStream out = new JarOutputStream(new FileOutputStream(this.file))) {
out.putNextEntry(new ZipEntry("test.dat"));
out.write(content.getBytes(StandardCharsets.UTF_8));
out.closeEntry();
}
JarUrlConnection connection = JarUrlConnection
.open(new URL("jar:file:" + this.file.getAbsolutePath() + "!/test.dat"));
assertThat(connection.getContentType()).isEqualTo("application/xml");
}
@Test
void getContentTypeWhenNotKnownInStreamButKnownNameReturnsDeducedType() throws Exception {
String content = "nothinguseful";
try (JarOutputStream out = new JarOutputStream(new FileOutputStream(this.file))) {
out.putNextEntry(new ZipEntry("test.xml"));
out.write(content.getBytes(StandardCharsets.UTF_8));
out.closeEntry();
}
JarUrlConnection connection = JarUrlConnection
.open(new URL("jar:file:" + this.file.getAbsolutePath() + "!/test.xml"));
assertThat(connection.getContentType()).isEqualTo("application/xml");
}
@Test
void getContentTypeWhenCannotBeDeducedReturnsContentUnknown() throws Exception {
String content = "nothinguseful";
try (JarOutputStream out = new JarOutputStream(new FileOutputStream(this.file))) {
out.putNextEntry(new ZipEntry("test.dat"));
out.write(content.getBytes(StandardCharsets.UTF_8));
out.closeEntry();
}
JarUrlConnection connection = JarUrlConnection
.open(new URL("jar:file:" + this.file.getAbsolutePath() + "!/test.dat"));
assertThat(connection.getContentType()).isEqualTo("content/unknown");
}
@Test
void getHeaderFieldDelegatesToJarFileConnection() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
URLConnection jarFileConnection = mock(URLConnection.class);
given(jarFileConnection.getHeaderField("test")).willReturn("test");
ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection);
assertThat(connection.getHeaderField("test")).isEqualTo("test");
}
@Test
void getContentWhenHasEntryReturnsContentFromEntry() throws Exception {
String content = "hello";
try (JarOutputStream out = new JarOutputStream(new FileOutputStream(this.file))) {
out.putNextEntry(new ZipEntry("test.txt"));
out.write(content.getBytes(StandardCharsets.UTF_8));
out.closeEntry();
}
JarUrlConnection connection = JarUrlConnection
.open(new URL("jar:file:" + this.file.getAbsolutePath() + "!/test.txt"));
assertThat(connection.getContent()).isInstanceOf(FilterInputStream.class);
}
@Test
void getContentWhenHasNoEntryReturnsJarFile() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
assertThat(connection.getContent()).isInstanceOf(JarFile.class);
}
@Test
void getPermissionReturnJarConnectionPermission() throws IOException {
JarUrlConnection connection = JarUrlConnection.open(this.url);
URLConnection jarFileConnection = mock(URLConnection.class);
Permission permission = mock(Permission.class);
given(jarFileConnection.getPermission()).willReturn(permission);
ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection);
assertThat(connection.getPermission()).isSameAs(permission);
}
@Test
void getInputStreamWhenHasNoEntryThrowsException() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
assertThatIOException().isThrownBy(() -> connection.getInputStream()).withMessage("no entry name specified");
}
@Test
void getInputStreamWhenOptimizedWithoutReadAndHasCachedJarWithEntryReturnsEmptyInputStream() throws Exception {
JarUrlConnection setupConnection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar"));
setupConnection.connect();
assertThat(JarUrlConnection.jarFiles.getCached(setupConnection.getJarFileURL())).isNotNull();
JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "3.dat"));
connection.setUseCaches(false);
Optimizations.enable(false);
assertThat(connection.getInputStream()).isSameAs(JarUrlConnection.emptyInputStream);
}
@Test
void getInputStreamWhenNoEntryAndOptimzedThrowsException() throws Exception {
JarUrlConnection setupConnection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar"));
setupConnection.connect();
assertThat(JarUrlConnection.jarFiles.getCached(setupConnection.getJarFileURL())).isNotNull();
JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "missing.dat"));
Optimizations.enable(false);
assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(connection::getInputStream)
.isSameAs(JarUrlConnection.FILE_NOT_FOUND_EXCEPTION);
}
@Test
void getInputStreamWhenNoEntryAndNotOptimzedThrowsException() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "missing.dat"));
assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(connection::getInputStream)
.withMessageContaining("JAR entry missing.dat not found in");
}
@Test
void getInputStreamReturnsInputStream() throws IOException {
JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "3.dat"));
try (InputStream in = connection.getInputStream()) {
assertThat(in).hasBinaryContent(new byte[] { 3 });
}
}
@Test
void getInputStreamWhenNoCachedClosesJarFileOnClose() throws IOException {
JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "3.dat"));
connection.setUseCaches(false);
InputStream in = connection.getInputStream();
JarFile jarFile = (JarFile) ReflectionTestUtils.getField(connection, "jarFile");
jarFile = spy(jarFile);
ReflectionTestUtils.setField(connection, "jarFile", jarFile);
in.close();
then(jarFile).should().close();
}
@Test
void getAllowUserInteractionDelegatesToJarFileConnection() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
URLConnection jarFileConnection = mock(URLConnection.class);
ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection);
given(jarFileConnection.getAllowUserInteraction()).willReturn(true);
assertThat(connection.getAllowUserInteraction()).isTrue();
then(jarFileConnection).should().getAllowUserInteraction();
}
@Test
void setAllowUserInteractionDelegatesToJarFileConnection() throws IOException {
JarUrlConnection connection = JarUrlConnection.open(this.url);
URLConnection jarFileConnection = mock(URLConnection.class);
ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection);
connection.setAllowUserInteraction(true);
then(jarFileConnection).should().setAllowUserInteraction(true);
}
@Test
void getUseCachesDelegatesToJarFileConnection() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
URLConnection jarFileConnection = mock(URLConnection.class);
ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection);
given(jarFileConnection.getUseCaches()).willReturn(true);
assertThat(connection.getUseCaches()).isTrue();
then(jarFileConnection).should().getUseCaches();
}
@Test
void setUseCachesDelegatesToJarFileConnection() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
URLConnection jarFileConnection = mock(URLConnection.class);
ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection);
connection.setUseCaches(true);
then(jarFileConnection).should().setUseCaches(true);
}
@Test
void getDefaultUseCachesDelegatesToJarFileConnection() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
URLConnection jarFileConnection = mock(URLConnection.class);
ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection);
given(jarFileConnection.getDefaultUseCaches()).willReturn(true);
assertThat(connection.getDefaultUseCaches()).isTrue();
then(jarFileConnection).should().getDefaultUseCaches();
}
@Test
void setDefaultUseCachesDelegatesToJarFileConnection() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
URLConnection jarFileConnection = mock(URLConnection.class);
ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection);
connection.setDefaultUseCaches(true);
then(jarFileConnection).should().setDefaultUseCaches(true);
}
@Test
void setIfModifiedSinceDelegatesToJarFileConnection() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
URLConnection jarFileConnection = mock(URLConnection.class);
ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection);
connection.setIfModifiedSince(123L);
then(jarFileConnection).should().setIfModifiedSince(123L);
}
@Test
void getRequestPropertyDelegatesToJarFileConnection() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
URLConnection jarFileConnection = mock(URLConnection.class);
ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection);
given(jarFileConnection.getRequestProperty("test")).willReturn("test");
assertThat(connection.getRequestProperty("test")).isEqualTo("test");
then(jarFileConnection).should().getRequestProperty("test");
}
@Test
void setRequestPropertyDelegatesToJarFileConnection() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
URLConnection jarFileConnection = mock(URLConnection.class);
ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection);
connection.setRequestProperty("test", "testvalue");
then(jarFileConnection).should().setRequestProperty("test", "testvalue");
}
@Test
void addRequestPropertyDelegatesToJarFileConnection() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
URLConnection jarFileConnection = mock(URLConnection.class);
ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection);
connection.addRequestProperty("test", "testvalue");
then(jarFileConnection).should().addRequestProperty("test", "testvalue");
}
@Test
void getRequestPropertiesDelegatesToJarFileConnection() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
URLConnection jarFileConnection = mock(URLConnection.class);
ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection);
Map<String, List<String>> properties = Map.of("test", List.of("testvalue"));
given(jarFileConnection.getRequestProperties()).willReturn(properties);
assertThat(connection.getRequestProperties()).isEqualTo(properties);
then(jarFileConnection).should().getRequestProperties();
}
@Test
void connectWhenConnectedDoesNotReconnect() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
connection.connect();
ReflectionTestUtils.setField(connection, "jarFile", null);
connection.connect();
assertThat(ReflectionTestUtils.getField(connection, "jarFile")).isNull();
}
@Test
void connectWhenHasNotFoundSupplierThrowsException() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "missing.dat"));
assertThat(connection).extracting("notFound").isNotNull();
assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(connection::connect)
.withMessageContaining("JAR entry missing.dat not found in");
}
@Test
void connectWhenOptimizationsEnabledAndHasCachedJarWithoutEntryThrowsException() throws Exception {
JarUrlConnection setupConnection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar"));
setupConnection.connect();
assertThat(JarUrlConnection.jarFiles.getCached(setupConnection.getJarFileURL())).isNotNull();
JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "missing.dat"));
Optimizations.enable(true);
assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(connection::connect)
.isSameAs(JarUrlConnection.FILE_NOT_FOUND_EXCEPTION);
}
@Test
void connectWhenHasNoEntryConnects() throws Exception {
JarUrlConnection setupConnection = JarUrlConnection.open(this.url);
setupConnection.connect();
assertThat(setupConnection.getJarFile()).isNotNull();
}
@Test
void connectWhenEntryDoesNotExistAndOptimizationsEnabledThrowsException() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "missing.dat"));
Optimizations.enable(true);
assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(connection::connect)
.isSameAs(JarUrlConnection.FILE_NOT_FOUND_EXCEPTION);
}
@Test
void connectWhenEntryDoesNotExistAndNoOptimizationsEnabledThrowsException() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "missing.dat"));
assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(connection::connect)
.withMessageContaining("JAR entry missing.dat not found in");
}
@Test
void connectWhenEntryExists() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "3.dat"));
connection.connect();
assertThat(connection.getJarEntry()).isNotNull();
}
@Test
void connectWhenAddedToCacheReconnects() throws IOException {
JarUrlConnection connection = JarUrlConnection.open(this.url);
Object originalConnection = ReflectionTestUtils.getField(connection, "jarFileConnection");
connection.connect();
assertThat(connection).extracting("jarFileConnection").isNotSameAs(originalConnection);
}
@Test
void openWhenNestedAndInCachedWithoutEntryAndOptimzationsEnabledReturnsNoFoundConnection() throws Exception {
JarUrlConnection setupConnection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar"));
setupConnection.connect();
assertThat(JarUrlConnection.jarFiles.getCached(setupConnection.getJarFileURL())).isNotNull();
Optimizations.enable(true);
JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "missing.dat"));
assertThat(connection).isSameAs(JarUrlConnection.NOT_FOUND_CONNECTION);
}
@Test
void openReturnsConnection() throws Exception {
JarUrlConnection connection = JarUrlConnection.open(this.url);
assertThat(connection).isNotNull();
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JarUrl}.
*
* @author Phillip Webb
*/
class JarUrlTests {
@TempDir
File temp;
File jarFile;
String jarFileUrlPath;
@BeforeEach
void setup() throws MalformedURLException {
this.jarFile = new File(this.temp, "my.jar");
this.jarFileUrlPath = this.temp.toURI().toURL().toString().substring("file:".length());
}
@Test
void createWithFileReturnsUrl() {
URL url = JarUrl.create(this.temp);
assertThat(url).hasToString("jar:file:%s!/".formatted(this.jarFileUrlPath));
}
@Test
void createWithFileAndEntryReturnsUrl() {
JarEntry entry = new JarEntry("lib.jar");
URL url = JarUrl.create(this.temp, entry);
assertThat(url).hasToString("jar:nested:%s/!lib.jar!/".formatted(this.jarFileUrlPath));
}
@Test
void createWithFileAndNullEntryReturnsUrl() {
URL url = JarUrl.create(this.temp, (JarEntry) null);
assertThat(url).hasToString("jar:file:%s!/".formatted(this.jarFileUrlPath));
}
@Test
void createWithFileAndNameReturnsUrl() {
URL url = JarUrl.create(this.temp, "lib.jar");
assertThat(url).hasToString("jar:nested:%s/!lib.jar!/".formatted(this.jarFileUrlPath));
}
@Test
void createWithFileAndNullNameReturnsUrl() {
URL url = JarUrl.create(this.temp, (String) null);
assertThat(url).hasToString("jar:file:%s!/".formatted(this.jarFileUrlPath));
}
@Test
void createWithFileNameAndPathReturnsUrl() {
URL url = JarUrl.create(this.temp, "lib.jar", "com/example/My.class");
assertThat(url).hasToString("jar:nested:%s/!lib.jar!/com/example/My.class".formatted(this.jarFileUrlPath));
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link LazyDelegatingInputStream}.
*
* @author Phillip Webb
*/
class LazyDelegatingInputStreamTests {
private InputStream delegate = mock(InputStream.class);
private TestLazyDelegatingInputStream inputStream = new TestLazyDelegatingInputStream();
@Test
void noOperationsDoesNotGetDelegateInputStream() {
then(this.delegate).shouldHaveNoInteractions();
}
@Test
void readDelegatesToInputStream() throws Exception {
this.inputStream.read();
then(this.delegate).should().read();
}
@Test
void readWithByteArrayDelegatesToInputStream() throws Exception {
byte[] bytes = new byte[1];
this.inputStream.read(bytes);
then(this.delegate).should().read(bytes);
}
@Test
void readWithByteArrayAndOffsetAndLenDelegatesToInputStream() throws Exception {
byte[] bytes = new byte[1];
this.inputStream.read(bytes, 0, 1);
then(this.delegate).should().read(bytes, 0, 1);
}
@Test
void skipDelegatesToInputStream() throws Exception {
this.inputStream.skip(10);
then(this.delegate).should().skip(10);
}
@Test
void availableDelegatesToInputStream() throws Exception {
this.inputStream.available();
then(this.delegate).should().available();
}
@Test
void markSupportedDelegatesToInputStream() {
this.inputStream.markSupported();
then(this.delegate).should().markSupported();
}
@Test
void markDelegatesToInputStream() {
this.inputStream.mark(10);
then(this.delegate).should().mark(10);
}
@Test
void resetDelegatesToInputStream() throws Exception {
this.inputStream.reset();
then(this.delegate).should().reset();
}
@Test
void closeWhenDelegateNotCreatedDoesNothing() throws Exception {
this.inputStream.close();
then(this.delegate).shouldHaveNoInteractions();
}
@Test
void closeDelegatesToInputStream() throws Exception {
this.inputStream.available();
this.inputStream.close();
then(this.delegate).should().close();
}
@Test
void getDelegateInputStreamIsOnlyCalledOnce() throws Exception {
this.inputStream.available();
this.inputStream.mark(10);
this.inputStream.read();
assertThat(this.inputStream.count).isOne();
}
private class TestLazyDelegatingInputStream extends LazyDelegatingInputStream {
private int count;
@Override
protected InputStream getDelegateInputStream() throws IOException {
this.count++;
return LazyDelegatingInputStreamTests.this.delegate;
}
}
}

View File

@@ -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.net.protocol.jar;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Optimizations}.
*
* @author Phillip Webb
*/
class OptimizationsTests {
@AfterEach
void reset() {
Optimizations.disable();
}
@Test
void defaultIsNotEnabled() {
assertThat(Optimizations.isEnabled()).isFalse();
assertThat(Optimizations.isEnabled(true)).isFalse();
assertThat(Optimizations.isEnabled(false)).isFalse();
}
@Test
void enableWithReadContentsEnables() {
Optimizations.enable(true);
assertThat(Optimizations.isEnabled()).isTrue();
assertThat(Optimizations.isEnabled(true)).isTrue();
assertThat(Optimizations.isEnabled(false)).isFalse();
}
@Test
void enableWithoutReadContentsEnables() {
Optimizations.enable(false);
assertThat(Optimizations.isEnabled()).isTrue();
assertThat(Optimizations.isEnabled(true)).isFalse();
assertThat(Optimizations.isEnabled(false)).isTrue();
}
@Test
void enableIsByThread() throws InterruptedException {
Optimizations.enable(true);
boolean[] enabled = new boolean[1];
Thread thread = new Thread(() -> enabled[0] = Optimizations.isEnabled());
thread.start();
thread.join();
assertThat(enabled[0]).isFalse();
}
@Test
void disableDisables() {
Optimizations.enable(true);
Optimizations.disable();
assertThat(Optimizations.isEnabled()).isFalse();
assertThat(Optimizations.isEnabled(true)).isFalse();
assertThat(Optimizations.isEnabled(false)).isFalse();
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.util.jar.Attributes;
import java.util.jar.JarEntry;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link UrlJarEntry}.
*
* @author Phillip Webb
*/
class UrlJarEntryTests {
@Test
void ofWhenEntryIsNullReturnsNull() {
assertThat(UrlJarEntry.of(null, null)).isNull();
}
@Test
void ofReturnsUrlJarEntry() {
JarEntry entry = new JarEntry("test");
assertThat(UrlJarEntry.of(entry, null)).isNotNull();
}
@Test
void getAttributesDelegatesToUrlJarManifest() throws Exception {
JarEntry entry = new JarEntry("test");
UrlJarManifest manifest = mock(UrlJarManifest.class);
Attributes attributes = mock(Attributes.class);
given(manifest.getEntryAttributes(any())).willReturn(attributes);
assertThat(UrlJarEntry.of(entry, manifest).getAttributes()).isSameAs(attributes);
}
}

View File

@@ -0,0 +1,114 @@
/*
* 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.FileInputStream;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.URL;
import java.util.function.Consumer;
import java.util.jar.JarFile;
import com.sun.net.httpserver.HttpServer;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.loader.net.protocol.Handlers;
import org.springframework.boot.loader.testsupport.TestJar;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link UrlJarFileFactory}.
*
* @author Phillip Webb
*/
class UrlJarFileFactoryTests {
@TempDir
File temp;
private final UrlJarFileFactory factory = new UrlJarFileFactory();
@Mock
private Consumer<JarFile> closeAction;
@BeforeAll
static void registerHandlers() {
Handlers.register();
}
@BeforeEach
void setup() {
MockitoAnnotations.openMocks(this);
}
@Test
void createJarFileWhenLocalFile() throws Throwable {
File file = new File(this.temp, "test.jar");
TestJar.create(file);
URL url = file.toURI().toURL();
JarFile jarFile = this.factory.createJarFile(url, this.closeAction);
assertThat(jarFile).isInstanceOf(UrlJarFile.class);
assertThat(jarFile).hasFieldOrPropertyWithValue("closeAction", this.closeAction);
}
@Test
void createJarFileWhenNested() throws Throwable {
File file = new File(this.temp, "test.jar");
TestJar.create(file);
URL url = new URL("nested:" + file.getPath() + "/!nested.jar");
JarFile jarFile = this.factory.createJarFile(url, this.closeAction);
assertThat(jarFile).isInstanceOf(UrlNestedJarFile.class);
assertThat(jarFile).hasFieldOrPropertyWithValue("closeAction", this.closeAction);
}
@Test
void createJarFileWhenStream() throws Exception {
File file = new File(this.temp, "test.jar");
TestJar.create(file);
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
server.createContext("/test", (exchange) -> {
exchange.sendResponseHeaders(200, file.length());
try (InputStream in = new FileInputStream(file)) {
in.transferTo(exchange.getResponseBody());
}
exchange.close();
});
server.start();
try {
URL url = new URL("http://localhost:" + server.getAddress().getPort() + "/test");
JarFile jarFile = this.factory.createJarFile(url, this.closeAction);
assertThat(jarFile).isInstanceOf(UrlJarFile.class);
assertThat(jarFile).hasFieldOrPropertyWithValue("closeAction", this.closeAction);
}
finally {
server.stop(0);
}
}
@Test
void createWhenHasRuntimeRef() {
}
}

View File

@@ -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.net.protocol.jar;
import java.io.File;
import java.util.function.Consumer;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
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.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.loader.testsupport.TestJar;
import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
/**
* Tests for {@link UrlJarFile}.
*
* @author Phillip Webb
*/
@AssertFileChannelDataBlocksClosed
class UrlJarFileTests {
@TempDir
File temp;
private UrlJarFile jarFile;
@Mock
private Consumer<JarFile> closeAction;
@BeforeEach
void setup() throws Exception {
MockitoAnnotations.openMocks(this);
File file = new File(this.temp, "test.jar");
TestJar.create(file);
this.jarFile = new UrlJarFile(file, Runtime.version(), this.closeAction);
}
@AfterEach
void cleanup() throws Exception {
this.jarFile.close();
}
@Test
void getEntryWhenNotfoundReturnsNull() {
assertThat(this.jarFile.getEntry("missing")).isNull();
}
@Test
void getEntryWhenFoundReturnsUrlJarEntry() {
assertThat(this.jarFile.getEntry("1.dat")).isInstanceOf(UrlJarEntry.class);
}
@Test
void getManifestReturnsNewCopy() throws Exception {
Manifest manifest1 = this.jarFile.getManifest();
Manifest manifest2 = this.jarFile.getManifest();
assertThat(manifest1).isNotSameAs(manifest2);
}
@Test
void closeCallsCloseAction() throws Exception {
this.jarFile.close();
then(this.closeAction).should().accept(this.jarFile);
}
}

View File

@@ -0,0 +1,164 @@
/*
* 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.URL;
import java.net.URLConnection;
import java.util.jar.JarFile;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.loader.net.protocol.Handlers;
import org.springframework.boot.loader.testsupport.TestJar;
import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
/**
* Tests for {@link UrlJarFiles}.
*
* @author Phillip Webb
*/
@AssertFileChannelDataBlocksClosed
class UrlJarFilesTests {
@TempDir
File temp;
private UrlJarFileFactory factory = mock(UrlJarFileFactory.class);
private final UrlJarFiles jarFiles = new UrlJarFiles(this.factory);
private File file;
private URL url;
@BeforeAll
static void registerHandlers() {
Handlers.register();
}
@BeforeEach
void setup() throws Exception {
this.file = new File(this.temp, "test.jar");
this.url = new URL("nested:" + this.file.getAbsolutePath() + "/!nested.jar");
TestJar.create(this.file);
}
@Test
void getOrCreateWhenNotUsingCachesAlwaysCreatesNewJarFile() throws Exception {
given(this.factory.createJarFile(any(), any())).willCallRealMethod();
JarFile jarFile1 = this.jarFiles.getOrCreate(false, this.url);
JarFile jarFile2 = this.jarFiles.getOrCreate(false, this.url);
JarFile jarFile3 = this.jarFiles.getOrCreate(false, this.url);
assertThat(jarFile1).isNotSameAs(jarFile2).isNotSameAs(jarFile3);
}
@Test
void getOrCreateWhenUsingCachingReturnsCachedWhenAvailable() throws Exception {
given(this.factory.createJarFile(any(), any())).willCallRealMethod();
JarFile jarFile1 = this.jarFiles.getOrCreate(true, this.url);
this.jarFiles.cacheIfAbsent(true, this.url, jarFile1);
JarFile jarFile2 = this.jarFiles.getOrCreate(true, this.url);
JarFile jarFile3 = this.jarFiles.getOrCreate(true, this.url);
assertThat(jarFile1).isSameAs(jarFile2).isSameAs(jarFile3);
}
@Test
void getCachedWhenNotCachedReturnsNull() {
assertThat(this.jarFiles.getCached(this.url)).isNull();
}
@Test
void getCachedWhenCachedReturnsCachedJar() throws Exception {
given(this.factory.createJarFile(any(), any())).willCallRealMethod();
JarFile jarFile = this.factory.createJarFile(this.url, null);
this.jarFiles.cacheIfAbsent(true, this.url, jarFile);
assertThat(this.jarFiles.getCached(this.url)).isSameAs(jarFile);
}
@Test
void cacheIfAbsentWhenNotUsingCachesDoesNotCacheAndReturnsFalse() throws Exception {
given(this.factory.createJarFile(any(), any())).willCallRealMethod();
JarFile jarFile = this.factory.createJarFile(this.url, null);
this.jarFiles.cacheIfAbsent(false, this.url, jarFile);
assertThat(this.jarFiles.getCached(this.url)).isNull();
}
@Test
void cacheIfAbsentWhenUsingCachingAndNotAlreadyCachedCachesAndReturnsTrue() throws Exception {
given(this.factory.createJarFile(any(), any())).willCallRealMethod();
JarFile jarFile = this.factory.createJarFile(this.url, null);
assertThat(this.jarFiles.cacheIfAbsent(true, this.url, jarFile)).isTrue();
assertThat(this.jarFiles.getCached(this.url)).isSameAs(jarFile);
}
@Test
void cacheIfAbsentWhenUsingCachingAndAlreadyCachedLeavesCacheAndReturnsFalse() throws Exception {
given(this.factory.createJarFile(any(), any())).willCallRealMethod();
JarFile jarFile1 = this.factory.createJarFile(this.url, null);
JarFile jarFile2 = this.factory.createJarFile(this.url, null);
assertThat(this.jarFiles.cacheIfAbsent(true, this.url, jarFile1)).isTrue();
assertThat(this.jarFiles.cacheIfAbsent(true, this.url, jarFile2)).isFalse();
assertThat(this.jarFiles.getCached(this.url)).isSameAs(jarFile1);
}
@Test
void closeIfNotCachedWhenNotCachedClosesJarFile() throws Exception {
JarFile jarFile = mock(JarFile.class);
this.jarFiles.closeIfNotCached(this.url, jarFile);
then(jarFile).should().close();
}
@Test
void closeIfNotCachedWhenCachedDoesNotCloseJarFile() throws Exception {
JarFile jarFile = mock(JarFile.class);
this.jarFiles.cacheIfAbsent(true, this.url, jarFile);
this.jarFiles.closeIfNotCached(this.url, jarFile);
then(jarFile).should(never()).close();
}
@Test
void reconnectReconnectsAndAppliesUseCaches() throws Exception {
JarFile jarFile = mock(JarFile.class);
this.jarFiles.cacheIfAbsent(true, this.url, jarFile);
URLConnection existingConnection = mock(URLConnection.class);
given(existingConnection.getUseCaches()).willReturn(true);
URLConnection connection = this.jarFiles.reconnect(jarFile, existingConnection);
assertThat(connection).isNotSameAs(existingConnection);
assertThat(connection.getUseCaches()).isTrue();
}
@Test
void reconnectWhenExistingConnectionIsNullReconnects() throws Exception {
JarFile jarFile = mock(JarFile.class);
this.jarFiles.cacheIfAbsent(true, this.url, jarFile);
URLConnection connection = this.jarFiles.reconnect(jarFile, null);
assertThat(connection).isNotNull();
assertThat(connection.getUseCaches()).isTrue();
}
}

View File

@@ -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.net.protocol.jar;
import java.io.IOException;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.Manifest;
import org.junit.jupiter.api.Test;
import org.springframework.boot.loader.net.protocol.jar.UrlJarManifest.ManifestSupplier;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
/**
* Tests for {@link UrlJarManifest}.
*
* @author Phillip Webb
*/
class UrlJarManifestTests {
@Test
void getWhenSuppliedManifestIsNullReturnsNull() throws Exception {
UrlJarManifest urlJarManifest = new UrlJarManifest(() -> null);
assertThat(urlJarManifest.get()).isNull();
}
@Test
void getAlwaysReturnsDeepCopy() throws Exception {
Manifest manifest = new Manifest();
UrlJarManifest urlJarManifest = new UrlJarManifest(() -> manifest);
manifest.getMainAttributes().putValue("test", "one");
manifest.getEntries().put("spring", new Attributes());
Manifest copy = urlJarManifest.get();
assertThat(copy).isNotSameAs(manifest);
manifest.getMainAttributes().clear();
manifest.getEntries().clear();
assertThat(copy.getMainAttributes()).isNotEmpty();
assertThat(copy.getAttributes("spring")).isNotNull();
}
@Test
void getEntrtyAttributesWhenSuppliedManifestIsNullReturnsNull() throws Exception {
UrlJarManifest urlJarManifest = new UrlJarManifest(() -> null);
assertThat(urlJarManifest.getEntryAttributes(new JarEntry("test"))).isNull();
}
@Test
void getEntryAttributesReturnsDeepCopy() throws Exception {
Manifest manifest = new Manifest();
UrlJarManifest urlJarManifest = new UrlJarManifest(() -> manifest);
Attributes attributes = new Attributes();
attributes.putValue("test", "test");
manifest.getEntries().put("spring", attributes);
Attributes copy = urlJarManifest.getEntryAttributes(new JarEntry("spring"));
assertThat(copy).isNotSameAs(attributes);
attributes.clear();
assertThat(copy.getValue("test")).isNotNull();
}
@Test
void supplierIsOnlyCalledOnce() throws IOException {
ManifestSupplier supplier = mock(ManifestSupplier.class);
UrlJarManifest urlJarManifest = new UrlJarManifest(supplier);
urlJarManifest.get();
urlJarManifest.get();
then(supplier).should(times(1)).getManifest();
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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.util.function.Consumer;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
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.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.loader.testsupport.TestJar;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
/**
* Tests for {@link UrlNestedJarFile}.
*
* @author Phillip Webb
*/
class UrlNestedJarFileTests {
@TempDir
File temp;
private UrlNestedJarFile jarFile;
@Mock
private Consumer<JarFile> closeAction;
@BeforeEach
void setup() throws Exception {
MockitoAnnotations.openMocks(this);
File file = new File(this.temp, "test.jar");
TestJar.create(file);
this.jarFile = new UrlNestedJarFile(file, "multi-release.jar", Runtime.version(), this.closeAction);
}
@AfterEach
void cleanup() throws Exception {
this.jarFile.close();
}
@Test
void getEntryWhenNotfoundReturnsNull() {
assertThat(this.jarFile.getEntry("missing")).isNull();
}
@Test
void getEntryWhenFoundReturnsUrlJarEntry() {
assertThat(this.jarFile.getEntry("multi-release.dat")).isInstanceOf(UrlJarEntry.class);
}
@Test
void getManifestReturnsNewCopy() throws Exception {
Manifest manifest1 = this.jarFile.getManifest();
Manifest manifest2 = this.jarFile.getManifest();
assertThat(manifest1).isNotSameAs(manifest2);
}
@Test
void closeCallsCloseAction() throws Exception {
this.jarFile.close();
then(this.closeAction).should().accept(this.jarFile);
}
}

View File

@@ -0,0 +1,77 @@
/*
* 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 org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.loader.net.protocol.Handlers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNoException;
/**
* Tests for {@link Handler}.
*
* @author Phillip Webb
*/
class HandlerTests {
@TempDir
File temp;
@BeforeAll
static void registerHandlers() {
Handlers.register();
}
@Test
void openConnectionReturnsNestedUrlConnection() throws Exception {
URL url = new URL("nested:" + this.temp.getAbsolutePath() + "/!nested.jar");
assertThat(url.openConnection()).isInstanceOf(NestedUrlConnection.class);
}
@Test
void assertUrlIsNotMalformedWhenUrlIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> Handler.assertUrlIsNotMalformed(null))
.withMessageContaining("'url' must not be null");
}
@Test
void assertUrlIsNotMalformedWhenUrlIsNotNestedThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> Handler.assertUrlIsNotMalformed("file:"))
.withMessageContaining("must use 'nested'");
}
@Test
void assertUrlIsNotMalformedWhenUrlIsMalformedThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> Handler.assertUrlIsNotMalformed("nested:bad"))
.withMessageContaining("'path' must contain '/!'");
}
@Test
void assertUrlIsNotMalformedWhenUrlIsValidDoesNotThrowException() {
String url = "nested:" + this.temp.getAbsolutePath() + "/!nested.jar";
assertThatNoException().isThrownBy(() -> Handler.assertUrlIsNotMalformed(url));
}
}

View File

@@ -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 org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.loader.net.protocol.Handlers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link NestedLocation}.
*
* @author Phillip Webb
*/
class NestedLocationTests {
@TempDir
File temp;
@BeforeAll
static void registerHandlers() {
Handlers.register();
}
@Test
void createWhenFileIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(null, "nested.jar"))
.withMessageContaining("'file' must not be null");
}
@Test
void createWhenNestedEntryNameIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(new File("test.jar"), null))
.withMessageContaining("'nestedEntryName' must not be empty");
}
@Test
void createWhenNestedEntryNameIsEmptyThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(new File("test.jar"), null))
.withMessageContaining("'nestedEntryName' must not be empty");
}
@Test
void fromUrlWhenUrlIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> NestedLocation.fromUrl(null))
.withMessageContaining("'url' must not be null");
}
@Test
void fromUrlWhenNotNestedProtocolThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> NestedLocation.fromUrl(new URL("file://test.jar")))
.withMessageContaining("must use 'nested' protocol");
}
@Test
void fromUrlWhenNoPathThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> NestedLocation.fromUrl(new URL("nested:")))
.withMessageContaining("'path' must not be empty");
}
@Test
void fromUrlWhenNoSeparatorThrowsExceptiuon() {
assertThatIllegalArgumentException()
.isThrownBy(() -> NestedLocation.fromUrl(new URL("nested:test.jar!nested.jar")))
.withMessageContaining("'path' must contain '/!'");
}
@Test
void fromUrlReturnsNestedLocation() throws Exception {
File file = new File(this.temp, "test.jar");
NestedLocation location = NestedLocation
.fromUrl(new URL("nested:" + file.getAbsolutePath() + "/!lib/nested.jar"));
assertThat(location.file()).isEqualTo(file);
assertThat(location.nestedEntryName()).isEqualTo("lib/nested.jar");
}
}

View File

@@ -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.net.protocol.nested;
import java.io.File;
import java.io.FilePermission;
import java.io.InputStream;
import java.lang.ref.Cleaner.Cleanable;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.Permission;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.loader.net.protocol.Handlers;
import org.springframework.boot.loader.ref.Cleaner;
import org.springframework.boot.loader.testsupport.TestJar;
import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed;
import org.springframework.boot.loader.zip.ZipContent;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link NestedUrlConnection}.
*
* @author Phillip Webb
*/
@AssertFileChannelDataBlocksClosed
class NestedUrlConnectionTests {
@TempDir
File temp;
private File jarFile;
private URL url;
@BeforeAll
static void registerHandlers() {
Handlers.register();
}
@BeforeEach
void setup() throws Exception {
this.jarFile = new File(this.temp, "test.jar");
TestJar.create(this.jarFile);
this.url = new URL("nested:" + this.jarFile.getAbsolutePath() + "/!nested.jar");
}
@Test
void createWhenMalformedUrlThrowsException() throws Exception {
URL url = new URL("nested:bad.jar");
assertThatExceptionOfType(MalformedURLException.class).isThrownBy(() -> new NestedUrlConnection(url))
.withMessage("'path' must contain '/!'");
}
@Test
void getContentLengthWhenContentLengthMoreThanMaxIntReturnsMinusOne() {
NestedUrlConnection connection = mock(NestedUrlConnection.class);
given(connection.getContentLength()).willCallRealMethod();
given(connection.getContentLengthLong()).willReturn((long) Integer.MAX_VALUE + 1);
assertThat(connection.getContentLength()).isEqualTo(-1);
}
@Test
void getContentLengthGetsContentLength() throws Exception {
NestedUrlConnection connection = new NestedUrlConnection(this.url);
try (ZipContent zipContent = ZipContent.open(this.jarFile.toPath())) {
int expectedSize = zipContent.getEntry("nested.jar").getUncompressedSize();
assertThat(connection.getContentLength()).isEqualTo(expectedSize);
}
}
@Test
void getContentLengthLongReturnsContentLength() throws Exception {
NestedUrlConnection connection = new NestedUrlConnection(this.url);
try (ZipContent zipContent = ZipContent.open(this.jarFile.toPath())) {
int expectedSize = zipContent.getEntry("nested.jar").getUncompressedSize();
assertThat(connection.getContentLengthLong()).isEqualTo(expectedSize);
}
}
@Test
void getContentTypeReturnsJavaJar() throws Exception {
NestedUrlConnection connection = new NestedUrlConnection(this.url);
assertThat(connection.getContentType()).isEqualTo("x-java/jar");
}
@Test
void getLastModifiedReturnsFileLastModified() throws Exception {
NestedUrlConnection connection = new NestedUrlConnection(this.url);
assertThat(connection.getLastModified()).isEqualTo(this.jarFile.lastModified());
}
@Test
void getPermissionReturnsFilePermission() throws Exception {
NestedUrlConnection connection = new NestedUrlConnection(this.url);
Permission permission = connection.getPermission();
assertThat(permission).isInstanceOf(FilePermission.class);
assertThat(permission.getName()).isEqualTo(this.jarFile.getCanonicalPath());
}
@Test
void getInputStreamReturnsContentOfNestedJar() throws Exception {
NestedUrlConnection connection = new NestedUrlConnection(this.url);
try (InputStream actual = connection.getInputStream()) {
try (ZipContent zipContent = ZipContent.open(this.jarFile.toPath())) {
try (InputStream expected = zipContent.getEntry("nested.jar").openContent().asInputStream()) {
assertThat(actual).hasSameContentAs(expected);
}
}
}
}
@Test
void inputStreamCloseCleansResource() throws Exception {
Cleaner cleaner = mock(Cleaner.class);
Cleanable cleanable = mock(Cleanable.class);
given(cleaner.register(any(), any())).willReturn(cleanable);
NestedUrlConnection connection = new NestedUrlConnection(this.url, cleaner);
connection.getInputStream().close();
then(cleanable).should().clean();
ArgumentCaptor<Runnable> actionCaptor = ArgumentCaptor.forClass(Runnable.class);
then(cleaner).should().register(any(), actionCaptor.capture());
actionCaptor.getValue().run();
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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 org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link UrlDecoder}.
*
* @author Phillip Webb
*/
class UrlDecoderTests {
@Test
void decodeWhenBasicString() {
assertThat(UrlDecoder.decode("a/b/C.class")).isEqualTo("a/b/C.class");
}
@Test
void decodeWhenHasSingleByteEncodedCharacters() {
assertThat(UrlDecoder.decode("%61/%62/%43.class")).isEqualTo("a/b/C.class");
}
@Test
void decodeWhenHasDoubleByteEncodedCharacters() {
assertThat(UrlDecoder.decode("%c3%a1/b/C.class")).isEqualTo("\u00e1/b/C.class");
}
@Test
void decodeWhenHasMixtureOfEncodedAndUnencodedDoubleByteCharacters() {
assertThat(UrlDecoder.decode("%c3%a1/b/\u00c7.class")).isEqualTo("\u00e1/b/\u00c7.class");
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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;
/**
* Utility that allows tests to set a tracker on {@link DefaultCleaner}.
*
* @author Phillip Webb
*/
public final class DefaultCleanerTracking {
private DefaultCleanerTracking() {
}
public static void set(Consumer<Cleanable> tracker) {
DefaultCleaner.tracker = tracker;
}
}

View File

@@ -14,12 +14,13 @@
* limitations under the License.
*/
package org.springframework.boot.loader;
package org.springframework.boot.loader.testsupport;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
@@ -28,11 +29,11 @@ import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
/**
* Creates a simple test jar.
* Support class to create or get test jars.
*
* @author Phillip Webb
*/
public abstract class TestJarCreator {
public abstract class TestJar {
private static final int BASE_VERSION = 8;
@@ -50,15 +51,22 @@ public abstract class TestJarCreator {
RUNTIME_VERSION = version;
}
public static void createTestJar(File file) throws Exception {
createTestJar(file, false);
public static void create(File file) throws Exception {
create(file, false);
}
public static void createTestJar(File file, boolean unpackNested) throws Exception {
public static void create(File file, boolean unpackNested) throws Exception {
create(file, unpackNested, false);
}
public static void create(File file, boolean unpackNested, boolean addSignatureFile) throws Exception {
FileOutputStream fileOutputStream = new FileOutputStream(file);
try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) {
jarOutputStream.setComment("outer");
writeManifest(jarOutputStream, "j1");
if (addSignatureFile) {
writeEntry(jarOutputStream, "META-INF/some.DSA", 0);
}
writeEntry(jarOutputStream, "1.dat", 1);
writeEntry(jarOutputStream, "2.dat", 2);
writeDirEntry(jarOutputStream, "d/");
@@ -72,6 +80,11 @@ public abstract class TestJarCreator {
}
}
public static List<String> expectedEntries() {
return List.of("META-INF/", "META-INF/MANIFEST.MF", "1.dat", "2.dat", "d/", "d/9.dat", "special/",
"special/\u00EB.dat", "nested.jar", "another-nested.jar", "space nested.jar", "multi-release.jar");
}
private static void writeNestedEntry(String name, boolean unpackNested, JarOutputStream jarOutputStream)
throws Exception {
writeNestedEntry(name, unpackNested, jarOutputStream, false);
@@ -148,4 +161,14 @@ public abstract class TestJarCreator {
jarOutputStream.closeEntry();
}
public static File getSigned() {
String[] entries = System.getProperty("java.class.path").split(System.getProperty("path.separator"));
for (String entry : entries) {
if (entry.contains("bcprov")) {
return new File(entry);
}
}
return null;
}
}

View File

@@ -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.util;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SystemPropertyUtils}.
*
* @author Dave Syer
*/
class SystemPropertyUtilsTests {
@BeforeEach
void init() {
System.setProperty("foo", "bar");
}
@AfterEach
void close() {
System.clearProperty("foo");
}
@Test
void testVanillaPlaceholder() {
assertThat(SystemPropertyUtils.resolvePlaceholders("${foo}")).isEqualTo("bar");
}
@Test
void testDefaultValue() {
assertThat(SystemPropertyUtils.resolvePlaceholders("${bar:foo}")).isEqualTo("foo");
}
@Test
void testNestedPlaceholder() {
assertThat(SystemPropertyUtils.resolvePlaceholders("${bar:${spam:foo}}")).isEqualTo("foo");
}
@Test
void testEnvVar() {
assertThat(SystemPropertyUtils.getProperty("lang")).isEqualTo(System.getenv("LANG"));
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ExtendWith;
/**
* Annotation that can be added to tests to assert that {@link FileChannelDataBlock} files
* are not left open.
*
* @author Phillip Webb
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ExtendWith(AssertFileChannelDataBlocksClosedExtension.class)
public @interface AssertFileChannelDataBlocksClosed {
}

View File

@@ -0,0 +1,88 @@
/*
* 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.lang.ref.Cleaner.Cleanable;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.springframework.boot.loader.ref.DefaultCleanerTracking;
import org.springframework.boot.loader.zip.FileChannelDataBlock.Tracker;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Extension for {@link AssertFileChannelDataBlocksClosed @TrackFileChannelDataBlock}.
*/
class AssertFileChannelDataBlocksClosedExtension implements BeforeEachCallback, AfterEachCallback {
private static OpenFilesTracker tracker = new OpenFilesTracker();
@Override
public void beforeEach(ExtensionContext context) throws Exception {
tracker.clear();
FileChannelDataBlock.tracker = tracker;
DefaultCleanerTracking.set(tracker::addedCleanable);
}
@Override
public void afterEach(ExtensionContext context) throws Exception {
tracker.assertAllClosed();
FileChannelDataBlock.tracker = null;
}
private static class OpenFilesTracker implements Tracker {
private final Set<Path> paths = new LinkedHashSet<>();
private final List<Cleanable> cleanup = new ArrayList<>();
@Override
public void openedFileChannel(Path path, FileChannel fileChannel) {
this.paths.add(path);
}
@Override
public void closedFileChannel(Path path, FileChannel fileChannel) {
this.paths.remove(path);
}
void clear() {
this.paths.clear();
this.cleanup.clear();
}
void assertAllClosed() {
this.cleanup.forEach(Cleanable::clean);
assertThat(this.paths).as("open paths").isEmpty();
}
private void addedCleanable(Cleanable cleanable) {
this.cleanup.add(cleanable);
}
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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.nio.ByteBuffer;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ByteArrayDataBlock}.
*
* @author Phillip Webb
*/
class ByteArrayDataBlockTests {
private final byte[] BYTES = { 0, 1, 2, 3, 4, 5, 6, 7 };
@Test
void sizeReturnsByteArrayLength() throws Exception {
ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(this.BYTES);
assertThat(dataBlock.size()).isEqualTo(this.BYTES.length);
}
@Test
void readPutsBytes() throws Exception {
ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(this.BYTES);
ByteBuffer dst = ByteBuffer.allocate(8);
int result = dataBlock.read(dst, 0);
assertThat(result).isEqualTo(8);
assertThat(dst.array()).containsExactly(this.BYTES);
}
@Test
void readWhenLessBytesThanRemainingInBufferPutsBytes() throws Exception {
ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(this.BYTES);
ByteBuffer dst = ByteBuffer.allocate(9);
int result = dataBlock.read(dst, 0);
assertThat(result).isEqualTo(8);
assertThat(dst.array()).containsExactly(0, 1, 2, 3, 4, 5, 6, 7, 0);
}
@Test
void readWhenLessRemainingInBufferThanLengthPutsBytes() throws Exception {
ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(this.BYTES);
ByteBuffer dst = ByteBuffer.allocate(7);
int result = dataBlock.read(dst, 0);
assertThat(result).isEqualTo(7);
assertThat(dst.array()).containsExactly(0, 1, 2, 3, 4, 5, 6);
}
@Test
void readWhenHasPosOffsetReadsBytes() throws Exception {
ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(this.BYTES);
ByteBuffer dst = ByteBuffer.allocate(3);
int result = dataBlock.read(dst, 4);
assertThat(result).isEqualTo(3);
assertThat(dst.array()).containsExactly(4, 5, 6);
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
import org.mockito.stubbing.Answer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.withSettings;
/**
* Tests for {@link DataBlock}.
*
* @author Phillip Webb
*/
class DataBlockTests {
@Test
void readFullyReadsAllBytesByCallingReadMultipleTimes() throws IOException {
DataBlock dataBlock = mock(DataBlock.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));
given(dataBlock.read(any(), anyLong()))
.will(putBytes(new byte[] { 0, 1 }, new byte[] { 2 }, new byte[] { 3, 4, 5 }));
ByteBuffer dst = ByteBuffer.allocate(6);
dataBlock.readFully(dst, 0);
assertThat(dst.array()).containsExactly(0, 1, 2, 3, 4, 5);
}
private Answer<?> putBytes(byte[]... bytes) {
AtomicInteger count = new AtomicInteger();
return (invocation) -> {
int index = count.getAndIncrement();
invocation.getArgument(0, ByteBuffer.class).put(bytes[index]);
return bytes.length;
};
}
@Test
void readFullyWhenReadReturnsNegativeResultThrowsException() throws Exception {
DataBlock dataBlock = mock(DataBlock.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));
given(dataBlock.read(any(), anyLong())).willReturn(-1);
ByteBuffer dst = ByteBuffer.allocate(8);
assertThatExceptionOfType(EOFException.class).isThrownBy(() -> dataBlock.readFully(dst, 0));
}
@Test
void asInputStreamReturnsDataBlockInputStream() {
DataBlock dataBlock = mock(DataBlock.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));
assertThat(dataBlock.asInputStream()).isInstanceOf(DataBlockInputStream.class);
}
}

View File

@@ -0,0 +1,238 @@
/*
* 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.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
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.zip.FileChannelDataBlock.Tracker;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link FileChannelDataBlock}.
*
* @author Phillip Webb
*/
class FileChannelDataBlockTests {
private static final byte[] CONTENT = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 };
@TempDir
File tempDir;
File tempFile;
@BeforeEach
void writeTempFile() throws IOException {
this.tempFile = new File(this.tempDir, "content");
Files.write(this.tempFile.toPath(), CONTENT);
}
@AfterEach
void resetTracker() {
FileChannelDataBlock.tracker = null;
}
@Test
void sizeReturnsFileSize() throws IOException {
try (FileChannelDataBlock block = createAndOpenBlock()) {
assertThat(block.size()).isEqualTo(CONTENT.length);
}
}
@Test
void readReadsFile() throws IOException {
try (FileChannelDataBlock block = createAndOpenBlock()) {
ByteBuffer buffer = ByteBuffer.allocate(CONTENT.length);
assertThat(block.read(buffer, 0)).isEqualTo(6);
assertThat(buffer.array()).containsExactly(CONTENT);
}
}
@Test
void readDoesNotReadPastEndOfFile() throws IOException {
try (FileChannelDataBlock block = createAndOpenBlock()) {
ByteBuffer buffer = ByteBuffer.allocate(CONTENT.length);
assertThat(block.read(buffer, 2)).isEqualTo(4);
assertThat(buffer.array()).containsExactly(0x02, 0x03, 0x04, 0x05, 0x0, 0x0);
}
}
@Test
void readWhenPosAtSizeReturnsMinusOne() throws IOException {
try (FileChannelDataBlock block = createAndOpenBlock()) {
ByteBuffer buffer = ByteBuffer.allocate(CONTENT.length);
assertThat(block.read(buffer, 6)).isEqualTo(-1);
}
}
@Test
void readWhenPosOverSizeReturnsMinusOne() throws IOException {
try (FileChannelDataBlock block = createAndOpenBlock()) {
ByteBuffer buffer = ByteBuffer.allocate(CONTENT.length);
assertThat(block.read(buffer, 7)).isEqualTo(-1);
}
}
@Test
void readWhenPosIsNegativeThrowsException() throws IOException {
try (FileChannelDataBlock block = createAndOpenBlock()) {
ByteBuffer buffer = ByteBuffer.allocate(CONTENT.length);
assertThatIllegalArgumentException().isThrownBy(() -> block.read(buffer, -1));
}
}
@Test
void sliceWhenOffsetIsNegativeThrowsException() throws IOException {
try (FileChannelDataBlock block = createAndOpenBlock()) {
assertThatIllegalArgumentException().isThrownBy(() -> block.slice(-1, 0))
.withMessage("Offset must not be negative");
}
}
@Test
void sliceWhenSizeIsNegativeThrowsException() throws IOException {
try (FileChannelDataBlock block = createAndOpenBlock()) {
assertThatIllegalArgumentException().isThrownBy(() -> block.slice(0, -1))
.withMessage("Size must not be negative and must be within bounds");
}
}
@Test
void sliceWhenSizeIsOutOfBoundsThrowsException() throws IOException {
try (FileChannelDataBlock block = createAndOpenBlock()) {
assertThatIllegalArgumentException().isThrownBy(() -> block.slice(2, 5))
.withMessage("Size must not be negative and must be within bounds");
}
}
@Test
void sliceReturnsSlice() throws IOException {
try (FileChannelDataBlock slice = createAndOpenBlock().slice(1, 4)) {
assertThat(slice.size()).isEqualTo(4);
ByteBuffer buffer = ByteBuffer.allocate(4);
assertThat(slice.read(buffer, 0)).isEqualTo(4);
assertThat(buffer.array()).containsExactly(0x01, 0x02, 0x03, 0x04);
}
}
@Test
void openAndCloseHandleReferenceCounting() throws IOException {
TestTracker tracker = new TestTracker();
FileChannelDataBlock.tracker = tracker;
FileChannelDataBlock block = createBlock();
assertThat(block).extracting("channel.referenceCount").isEqualTo(0);
tracker.assertOpenCloseCounts(0, 0);
block.open();
assertThat(block).extracting("channel.referenceCount").isEqualTo(1);
tracker.assertOpenCloseCounts(1, 0);
block.open();
assertThat(block).extracting("channel.referenceCount").isEqualTo(2);
tracker.assertOpenCloseCounts(1, 0);
block.close();
assertThat(block).extracting("channel.referenceCount").isEqualTo(1);
tracker.assertOpenCloseCounts(1, 0);
block.close();
assertThat(block).extracting("channel.referenceCount").isEqualTo(0);
tracker.assertOpenCloseCounts(1, 1);
block.open();
assertThat(block).extracting("channel.referenceCount").isEqualTo(1);
tracker.assertOpenCloseCounts(2, 1);
block.close();
assertThat(block).extracting("channel.referenceCount").isEqualTo(0);
tracker.assertOpenCloseCounts(2, 2);
}
@Test
void openAndCloseSliceHandleReferenceCounting() throws IOException {
TestTracker tracker = new TestTracker();
FileChannelDataBlock.tracker = tracker;
FileChannelDataBlock block = createBlock();
FileChannelDataBlock slice = block.slice(1, 4);
assertThat(block).extracting("channel.referenceCount").isEqualTo(0);
tracker.assertOpenCloseCounts(0, 0);
block.open();
assertThat(block).extracting("channel.referenceCount").isEqualTo(1);
tracker.assertOpenCloseCounts(1, 0);
slice.open();
assertThat(slice).extracting("channel.referenceCount").isEqualTo(2);
tracker.assertOpenCloseCounts(1, 0);
slice.open();
assertThat(slice).extracting("channel.referenceCount").isEqualTo(3);
tracker.assertOpenCloseCounts(1, 0);
slice.close();
assertThat(slice).extracting("channel.referenceCount").isEqualTo(2);
tracker.assertOpenCloseCounts(1, 0);
slice.close();
assertThat(slice).extracting("channel.referenceCount").isEqualTo(1);
tracker.assertOpenCloseCounts(1, 0);
block.close();
assertThat(block).extracting("channel.referenceCount").isEqualTo(0);
tracker.assertOpenCloseCounts(1, 1);
slice.open();
assertThat(slice).extracting("channel.referenceCount").isEqualTo(1);
tracker.assertOpenCloseCounts(2, 1);
slice.close();
assertThat(slice).extracting("channel.referenceCount").isEqualTo(0);
tracker.assertOpenCloseCounts(2, 2);
}
private FileChannelDataBlock createAndOpenBlock() throws IOException {
FileChannelDataBlock block = createBlock();
block.open();
return block;
}
private FileChannelDataBlock createBlock() throws IOException {
return new FileChannelDataBlock(this.tempFile.toPath());
}
static class TestTracker implements Tracker {
private int openCount;
private int closeCount;
@Override
public void openedFileChannel(Path path, FileChannel fileChannel) {
this.openCount++;
}
@Override
public void closedFileChannel(Path path, FileChannel fileChannel) {
this.closeCount++;
}
void assertOpenCloseCounts(int expectedOpenCount, int expectedCloseCount) {
assertThat(this.openCount).as("openCount").isEqualTo(expectedOpenCount);
assertThat(this.closeCount).as("closeCount").isEqualTo(expectedCloseCount);
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link VirtualDataBlock}.
*
* @author Phillip Webb
*/
class VirtualDataBlockTests {
private VirtualDataBlock virtualDataBlock;
@BeforeEach
void setup() throws IOException {
List<DataBlock> subsections = new ArrayList<>();
subsections.add(new ByteArrayDataBlock("abc".getBytes(StandardCharsets.UTF_8)));
subsections.add(new ByteArrayDataBlock("defg".getBytes(StandardCharsets.UTF_8)));
subsections.add(new ByteArrayDataBlock("h".getBytes(StandardCharsets.UTF_8)));
this.virtualDataBlock = new VirtualDataBlock(subsections);
}
@Test
void sizeReturnsSize() throws IOException {
assertThat(this.virtualDataBlock.size()).isEqualTo(8);
}
@Test
void readFullyReadsAllBlocks() throws IOException {
ByteBuffer dst = ByteBuffer.allocate((int) this.virtualDataBlock.size());
this.virtualDataBlock.readFully(dst, 0);
assertThat(dst.array()).containsExactly("abcdefgh".getBytes(StandardCharsets.UTF_8));
}
@Test
void readWithShortBlock() throws IOException {
ByteBuffer dst = ByteBuffer.allocate(2);
assertThat(this.virtualDataBlock.read(dst, 1)).isEqualTo(2);
assertThat(dst.array()).containsExactly("bc".getBytes(StandardCharsets.UTF_8));
}
@Test
void readWithShortBlockAcrossSubsections() throws IOException {
ByteBuffer dst = ByteBuffer.allocate(3);
assertThat(this.virtualDataBlock.read(dst, 2)).isEqualTo(3);
assertThat(dst.array()).containsExactly("cde".getBytes(StandardCharsets.UTF_8));
}
@Test
void readWithBigBlock() throws IOException {
ByteBuffer dst = ByteBuffer.allocate(16);
assertThat(this.virtualDataBlock.read(dst, 1)).isEqualTo(7);
assertThat(dst.array()).startsWith("bcdefgh".getBytes(StandardCharsets.UTF_8));
}
}

View File

@@ -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.zip;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.loader.testsupport.TestJar;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link VirtualZipDataBlock}.
*
* @author Phillip Webb
*/
@AssertFileChannelDataBlocksClosed
class VirtualZipDataBlockTests {
@TempDir
File tempDir;
private File file;
@BeforeEach
void setup() throws Exception {
this.file = new File(this.tempDir, "test.jar");
TestJar.create(this.file);
}
@Test
void createContainsValidZipContent() throws IOException {
FileChannelDataBlock data = new FileChannelDataBlock(this.file.toPath());
data.open();
List<ZipCentralDirectoryFileHeaderRecord> centralRecords = new ArrayList<>();
List<Long> centralRecordPositions = new ArrayList<>();
ZipEndOfCentralDirectoryRecord eocd = ZipEndOfCentralDirectoryRecord.load(data).endOfCentralDirectoryRecord();
long pos = eocd.offsetToStartOfCentralDirectory();
for (int i = 0; i < eocd.totalNumberOfCentralDirectoryEntries(); i++) {
ZipCentralDirectoryFileHeaderRecord centralRecord = ZipCentralDirectoryFileHeaderRecord.load(data, pos);
String name = ZipString.readString(data, pos + ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET,
centralRecord.fileNameLength());
if (name.endsWith(".jar")) {
centralRecords.add(centralRecord);
centralRecordPositions.add(pos);
}
pos += centralRecord.size();
}
NameOffsetLookups nameOffsetLookups = new NameOffsetLookups(2, centralRecords.size());
for (int i = 0; i < centralRecords.size(); i++) {
nameOffsetLookups.enable(i, true);
}
nameOffsetLookups.enable(0, true);
File outputFile = new File(this.tempDir, "out.jar");
try (VirtualZipDataBlock block = new VirtualZipDataBlock(data, nameOffsetLookups,
centralRecords.toArray(ZipCentralDirectoryFileHeaderRecord[]::new),
centralRecordPositions.stream().mapToLong(Long::longValue).toArray())) {
try (FileOutputStream out = new FileOutputStream(outputFile)) {
block.asInputStream().transferTo(out);
}
}
try (FileSystem fileSystem = FileSystems.newFileSystem(outputFile.toPath())) {
assertThatExceptionOfType(NoSuchFileException.class)
.isThrownBy(() -> Files.size(fileSystem.getPath("nessted.jar")));
assertThat(Files.size(fileSystem.getPath("sted.jar"))).isGreaterThan(0);
assertThat(Files.size(fileSystem.getPath("other-nested.jar"))).isGreaterThan(0);
assertThat(Files.size(fileSystem.getPath("ace nested.jar"))).isGreaterThan(0);
assertThat(Files.size(fileSystem.getPath("lti-release.jar"))).isGreaterThan(0);
}
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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 org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Zip64EndOfCentralDirectoryLocator}.
*
* @author Phillip Webb
*/
class Zip64EndOfCentralDirectoryLocatorTests {
@Test
void findReturnsRecord() throws Exception {
DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { //
0x50, 0x4b, 0x06, 0x07, //
0x01, 0x00, 0x00, 0x00, //
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x03, 0x00, 0x00, 0x00 }); //
Zip64EndOfCentralDirectoryLocator eocd = Zip64EndOfCentralDirectoryLocator.find(dataBlock, 20);
assertThat(eocd.pos()).isEqualTo(0);
assertThat(eocd.numberOfThisDisk()).isEqualTo(1);
assertThat(eocd.offsetToZip64EndOfCentralDirectoryRecord()).isEqualTo(2);
assertThat(eocd.totalNumberOfDisks()).isEqualTo(3);
}
@Test
void findWhenSignatureDoesNotMatchReturnsNull() throws IOException {
DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { //
0x51, 0x4b, 0x06, 0x07, //
0x01, 0x00, 0x00, 0x00, //
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x03, 0x00, 0x00, 0x00 }); //
Zip64EndOfCentralDirectoryLocator eocd = Zip64EndOfCentralDirectoryLocator.find(dataBlock, 20);
assertThat(eocd).isNull();
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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 org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIOException;
/**
* Tests for {@link Zip64EndOfCentralDirectoryRecord}.
*
* @author Phillip Webb
*/
class Zip64EndOfCentralDirectoryRecordTests {
@Test
void loadLoadsData() throws Exception {
DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { //
0x50, 0x4b, 0x06, 0x06, //
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x02, 0x00, //
0x03, 0x00, //
0x04, 0x00, 0x00, 0x00, //
0x05, 0x00, 0x00, 0x00, //
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); //
Zip64EndOfCentralDirectoryLocator locator = new Zip64EndOfCentralDirectoryLocator(56, 0, 0, 0);
Zip64EndOfCentralDirectoryRecord eocd = Zip64EndOfCentralDirectoryRecord.load(dataBlock, locator);
assertThat(eocd.size()).isEqualTo(56);
assertThat(eocd.sizeOfZip64EndOfCentralDirectoryRecord()).isEqualTo(1);
assertThat(eocd.versionMadeBy()).isEqualTo((short) 2);
assertThat(eocd.versionNeededToExtract()).isEqualTo((short) 3);
assertThat(eocd.numberOfThisDisk()).isEqualTo(4);
assertThat(eocd.diskWhereCentralDirectoryStarts()).isEqualTo(5);
assertThat(eocd.numberOfCentralDirectoryEntriesOnThisDisk()).isEqualTo(6);
assertThat(eocd.totalNumberOfCentralDirectoryEntries()).isEqualTo(7);
assertThat(eocd.sizeOfCentralDirectory()).isEqualTo(8);
assertThat(eocd.offsetToStartOfCentralDirectory());
}
@Test
void loadWhenSignatureDoesNotMatchThrowsException() {
DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { //
0x51, 0x4b, 0x06, 0x06, //
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x02, 0x00, //
0x03, 0x00, //
0x04, 0x00, 0x00, 0x00, //
0x05, 0x00, 0x00, 0x00, //
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); //
Zip64EndOfCentralDirectoryLocator locator = new Zip64EndOfCentralDirectoryLocator(56, 0, 0, 0);
assertThatIOException().isThrownBy(() -> Zip64EndOfCentralDirectoryRecord.load(dataBlock, locator))
.withMessageContaining("Zip64 'End Of Central Directory Record' not found at position");
}
}

View File

@@ -0,0 +1,213 @@
/*
* 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.zip.ZipEntry;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIOException;
/**
* Tests for {@link ZipCentralDirectoryFileHeaderRecord}.
*
* @author Phillip Webb
*/
class ZipCentralDirectoryFileHeaderRecordTests {
@Test
void loadLoadsData() throws Exception {
DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { //
0x50, 0x4b, 0x01, 0x02, //
0x01, 0x00, //
0x02, 0x00, //
0x03, 0x00, //
0x04, 0x00, //
0x05, 0x00, //
0x06, 0x00, //
0x07, 0x00, 0x00, 0x00, //
0x08, 0x00, 0x00, 0x00, //
0x09, 0x00, 0x00, 0x00, //
0x0A, 0x00, //
0x0B, 0x00, //
0x0C, 0x00, //
0x0D, 0x00, //
0x0E, 0x00, //
0x0F, 0x00, 0x00, 0x00, //
0x10, 0x00, 0x00, 0x00 }); //
ZipCentralDirectoryFileHeaderRecord record = ZipCentralDirectoryFileHeaderRecord.load(dataBlock, 0);
assertThat(record.versionMadeBy()).isEqualTo((short) 1);
assertThat(record.versionNeededToExtract()).isEqualTo((short) 2);
assertThat(record.generalPurposeBitFlag()).isEqualTo((short) 3);
assertThat(record.compressionMethod()).isEqualTo((short) 4);
assertThat(record.lastModFileTime()).isEqualTo((short) 5);
assertThat(record.lastModFileDate()).isEqualTo((short) 6);
assertThat(record.crc32()).isEqualTo(7);
assertThat(record.compressedSize()).isEqualTo(8);
assertThat(record.uncompressedSize()).isEqualTo(9);
assertThat(record.fileNameLength()).isEqualTo((short) 10);
assertThat(record.extraFieldLength()).isEqualTo((short) 11);
assertThat(record.fileCommentLength()).isEqualTo((short) 12);
assertThat(record.diskNumberStart()).isEqualTo((short) 13);
assertThat(record.internalFileAttributes()).isEqualTo((short) 14);
assertThat(record.externalFileAttributes()).isEqualTo(15);
assertThat(record.offsetToLocalHeader()).isEqualTo(16);
}
@Test
void loadWhenSignatureDoesNotMatchThrowsException() {
DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { //
0x51, 0x4b, 0x01, 0x02, //
0x01, 0x00, //
0x02, 0x00, //
0x03, 0x00, //
0x04, 0x00, //
0x05, 0x00, //
0x06, 0x00, //
0x07, 0x00, 0x00, 0x00, //
0x08, 0x00, 0x00, 0x00, //
0x09, 0x00, 0x00, 0x00, //
0x0A, 0x00, //
0x0B, 0x00, //
0x0C, 0x00, //
0x0D, 0x00, //
0x0E, 0x00, //
0x0F, 0x00, 0x00, 0x00, //
0x10, 0x00, 0x00, 0x00 }); //
assertThatIOException().isThrownBy(() -> ZipCentralDirectoryFileHeaderRecord.load(dataBlock, 0))
.withMessageContaining("'Central Directory File Header Record' not found");
}
@Test
void sizeReturnsSize() {
ZipCentralDirectoryFileHeaderRecord record = new ZipCentralDirectoryFileHeaderRecord((short) 1, (short) 2,
(short) 3, (short) 4, (short) 5, (short) 6, 7, 8, 9, (short) 10, (short) 11, (short) 12, (short) 13,
(short) 14, 15, 16);
assertThat(record.size()).isEqualTo(79L);
}
@Test
void copyToCopiesDataToZipEntry() throws Exception {
DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { //
0x50, 0x4b, 0x01, 0x02, //
0x00, 0x00, //
0x00, 0x00, //
0x00, 0x00, //
0x08, 0x00, //
0x23, 0x74, //
0x58, 0x36, //
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, //
0x01, 0x00, 0x00, 0x00, //
0x02, 0x00, 0x00, 0x00, //
0x01, 0x00, //
0x01, 0x00, //
0x01, 0x00, //
0x00, 0x00, //
0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, //
0x61, //
0x62, //
0x63 }); //
ZipCentralDirectoryFileHeaderRecord record = ZipCentralDirectoryFileHeaderRecord.load(dataBlock, 0);
ZipEntry entry = new ZipEntry("");
record.copyTo(dataBlock, 0, entry);
assertThat(entry.getMethod()).isEqualTo(ZipEntry.DEFLATED);
assertThat(entry.getTimeLocal()).hasYear(2007);
assertThat(entry.getTime()).isEqualTo(1172356386000L);
assertThat(entry.getCrc()).isEqualTo(0xFFFFFFFFL);
assertThat(entry.getCompressedSize()).isEqualTo(1);
assertThat(entry.getSize()).isEqualTo(2);
assertThat(entry.getExtra()).containsExactly(0x62);
assertThat(entry.getComment()).isEqualTo("c");
}
@Test
void withFileNameLengthReturnsUpdatedInstance() {
ZipCentralDirectoryFileHeaderRecord record = new ZipCentralDirectoryFileHeaderRecord((short) 1, (short) 2,
(short) 3, (short) 4, (short) 5, (short) 6, 7, 8, 9, (short) 10, (short) 11, (short) 12, (short) 13,
(short) 14, 15, 16)
.withFileNameLength((short) 100);
assertThat(record.versionMadeBy()).isEqualTo((short) 1);
assertThat(record.versionNeededToExtract()).isEqualTo((short) 2);
assertThat(record.generalPurposeBitFlag()).isEqualTo((short) 3);
assertThat(record.compressionMethod()).isEqualTo((short) 4);
assertThat(record.lastModFileTime()).isEqualTo((short) 5);
assertThat(record.lastModFileDate()).isEqualTo((short) 6);
assertThat(record.crc32()).isEqualTo(7);
assertThat(record.compressedSize()).isEqualTo(8);
assertThat(record.uncompressedSize()).isEqualTo(9);
assertThat(record.fileNameLength()).isEqualTo((short) 100);
assertThat(record.extraFieldLength()).isEqualTo((short) 11);
assertThat(record.fileCommentLength()).isEqualTo((short) 12);
assertThat(record.diskNumberStart()).isEqualTo((short) 13);
assertThat(record.internalFileAttributes()).isEqualTo((short) 14);
assertThat(record.externalFileAttributes()).isEqualTo(15);
assertThat(record.offsetToLocalHeader()).isEqualTo(16);
}
@Test
void withOffsetToLocalHeaderReturnsUpdatedInstance() {
ZipCentralDirectoryFileHeaderRecord record = new ZipCentralDirectoryFileHeaderRecord((short) 1, (short) 2,
(short) 3, (short) 4, (short) 5, (short) 6, 7, 8, 9, (short) 10, (short) 11, (short) 12, (short) 13,
(short) 14, 15, 16)
.withOffsetToLocalHeader(100);
assertThat(record.versionMadeBy()).isEqualTo((short) 1);
assertThat(record.versionNeededToExtract()).isEqualTo((short) 2);
assertThat(record.generalPurposeBitFlag()).isEqualTo((short) 3);
assertThat(record.compressionMethod()).isEqualTo((short) 4);
assertThat(record.lastModFileTime()).isEqualTo((short) 5);
assertThat(record.lastModFileDate()).isEqualTo((short) 6);
assertThat(record.crc32()).isEqualTo(7);
assertThat(record.compressedSize()).isEqualTo(8);
assertThat(record.uncompressedSize()).isEqualTo(9);
assertThat(record.fileNameLength()).isEqualTo((short) 10);
assertThat(record.extraFieldLength()).isEqualTo((short) 11);
assertThat(record.fileCommentLength()).isEqualTo((short) 12);
assertThat(record.diskNumberStart()).isEqualTo((short) 13);
assertThat(record.internalFileAttributes()).isEqualTo((short) 14);
assertThat(record.externalFileAttributes()).isEqualTo(15);
assertThat(record.offsetToLocalHeader()).isEqualTo(100);
}
@Test
void asByteArrayReturnsByteArray() throws Exception {
byte[] bytes = new byte[] { //
0x50, 0x4b, 0x01, 0x02, //
0x01, 0x00, //
0x02, 0x00, //
0x03, 0x00, //
0x04, 0x00, //
0x05, 0x00, //
0x06, 0x00, //
0x07, 0x00, 0x00, 0x00, //
0x08, 0x00, 0x00, 0x00, //
0x09, 0x00, 0x00, 0x00, //
0x0A, 0x00, //
0x0B, 0x00, //
0x0C, 0x00, //
0x0D, 0x00, //
0x0E, 0x00, //
0x0F, 0x00, 0x00, 0x00, //
0x10, 0x00, 0x00, 0x00 };
DataBlock dataBlock = new ByteArrayDataBlock(bytes);
ZipCentralDirectoryFileHeaderRecord record = ZipCentralDirectoryFileHeaderRecord.load(dataBlock, 0);
assertThat(record.asByteArray()).containsExactly(bytes);
}
}

View File

@@ -0,0 +1,437 @@
/*
* 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.Iterator;
import java.util.Random;
import java.util.jar.Manifest;
import java.util.zip.CRC32;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.loader.testsupport.TestJar;
import org.springframework.boot.loader.zip.ZipContent.Entry;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIOException;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link ZipContent}.
*
* @author Phillip Webb
* @author Martin Lau
* @author Andy Wilkinson
* @author Madhura Bhave
*/
class ZipContentTests {
@TempDir
File tempDir;
private File file;
private ZipContent zipContent;
@BeforeEach
void setup() throws Exception {
this.file = new File(this.tempDir, "test.jar");
TestJar.create(this.file);
this.zipContent = ZipContent.open(this.file.toPath());
}
@AfterEach
void tearDown() throws Exception {
if (this.zipContent != null) {
try {
this.zipContent.close();
}
catch (IllegalStateException ex) {
}
}
}
@Test
void getCommentReturnsComment() {
assertThat(this.zipContent.getComment()).isEqualTo("outer");
}
@Test
void getCommentWhenClosedThrowsException() throws IOException {
this.zipContent.close();
assertThatIllegalStateException().isThrownBy(() -> this.zipContent.getComment())
.withMessage("Zip content closed");
}
@Test
void getEntryWhenPresentReturnsEntry() {
Entry entry = this.zipContent.getEntry("1.dat");
assertThat(entry).isNotNull();
assertThat(entry.getName()).isEqualTo("1.dat");
}
@Test
void getEntryWhenMissingReturnsNull() {
assertThat(this.zipContent.getEntry("missing.dat")).isNull();
}
@Test
void getEntryWithPrefixWhenPresentReturnsEntry() {
Entry entry = this.zipContent.getEntry("1", ".dat");
assertThat(entry).isNotNull();
assertThat(entry.getName()).isEqualTo("1.dat");
}
@Test
void getEntryWithLongPrefixWhenNameIsShorterReturnsNull() {
Entry entry = this.zipContent.getEntry("iamaverylongprefixandiwontfindanything", "1.dat");
assertThat(entry).isNull();
}
@Test
void getEntryWithPrefixWhenMissingReturnsNull() {
assertThat(this.zipContent.getEntry("miss", "ing.dat")).isNull();
}
@Test
void getEntryWhenUsingSlashesIsCompatibleWithZipFile() throws IOException {
try (ZipFile zipFile = new ZipFile(this.file)) {
assertThat(zipFile.getEntry("META-INF").getName()).isEqualTo("META-INF/");
assertThat(this.zipContent.getEntry("META-INF").getName()).isEqualTo("META-INF/");
assertThat(zipFile.getEntry("META-INF/").getName()).isEqualTo("META-INF/");
assertThat(this.zipContent.getEntry("META-INF/").getName()).isEqualTo("META-INF/");
assertThat(zipFile.getEntry("d/9.dat").getName()).isEqualTo("d/9.dat");
assertThat(this.zipContent.getEntry("d/9.dat").getName()).isEqualTo("d/9.dat");
assertThat(zipFile.getEntry("d/9.dat/")).isNull();
assertThat(this.zipContent.getEntry("d/9.dat/")).isNull();
}
}
@Test
void getManifestEntry() throws Exception {
Entry entry = this.zipContent.getEntry("META-INF/MANIFEST.MF");
try (CloseableDataBlock dataBlock = entry.openContent()) {
Manifest manifest = new Manifest(asInflaterInputStream(dataBlock));
assertThat(manifest.getMainAttributes().getValue("Built-By")).isEqualTo("j1");
}
}
@Test
void getEntryAsCreatesCompatibleEntries() throws IOException {
try (ZipFile zipFile = new ZipFile(this.file)) {
Iterator<? extends ZipEntry> expected = zipFile.entries().asIterator();
int i = 0;
while (expected.hasNext()) {
Entry actual = this.zipContent.getEntry(i++);
assertThatFieldsAreEqual(actual.as(ZipEntry::new), expected.next());
}
}
}
private void assertThatFieldsAreEqual(ZipEntry actual, ZipEntry expected) {
assertThat(actual.getName()).isEqualTo(expected.getName());
assertThat(actual.getTime()).isEqualTo(expected.getTime());
assertThat(actual.getLastModifiedTime()).isEqualTo(expected.getLastModifiedTime());
assertThat(actual.getLastAccessTime()).isEqualTo(expected.getLastAccessTime());
assertThat(actual.getCreationTime()).isEqualTo(expected.getCreationTime());
assertThat(actual.getSize()).isEqualTo(expected.getSize());
assertThat(actual.getCompressedSize()).isEqualTo(expected.getCompressedSize());
assertThat(actual.getCrc()).isEqualTo(expected.getCrc());
assertThat(actual.getMethod()).isEqualTo(expected.getMethod());
assertThat(actual.getExtra()).isEqualTo(expected.getExtra());
assertThat(actual.getComment()).isEqualTo(expected.getComment());
}
@Test
void sizeReturnsNumberOfEntries() {
assertThat(this.zipContent.size()).isEqualTo(12);
}
@Test
void nestedJarFileReturnsNestedJar() throws IOException {
try (ZipContent nested = ZipContent.open(this.file.toPath(), "nested.jar")) {
assertThat(nested.size()).isEqualTo(5);
assertThat(nested.getComment()).isEqualTo("nested");
assertThat(nested.size()).isEqualTo(5);
assertThat(nested.getEntry(0).getName()).isEqualTo("META-INF/");
assertThat(nested.getEntry(1).getName()).isEqualTo("META-INF/MANIFEST.MF");
assertThat(nested.getEntry(2).getName()).isEqualTo("3.dat");
assertThat(nested.getEntry(3).getName()).isEqualTo("4.dat");
assertThat(nested.getEntry(4).getName()).isEqualTo("\u00E4.dat");
}
}
@Test
void nestedJarFileWhenNameEndsInSlashThrowsException() {
assertThatIOException().isThrownBy(() -> ZipContent.open(this.file.toPath(), "nested.jar/"))
.withMessageStartingWith("Nested entry 'nested.jar/' not found in container zip");
}
@Test
void nestedDirectoryReturnsNestedJar() throws IOException {
try (ZipContent nested = ZipContent.open(this.file.toPath(), "d/")) {
assertThat(nested.size()).isEqualTo(3);
assertThat(nested.getEntry("9.dat")).isNotNull();
assertThat(nested.getEntry(0).getName()).isEqualTo("META-INF/");
assertThat(nested.getEntry(1).getName()).isEqualTo("META-INF/MANIFEST.MF");
assertThat(nested.getEntry(2).getName()).isEqualTo("9.dat");
}
}
@Test
void nestedDirectoryWhenNotEndingInSlashThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> ZipContent.open(this.file.toPath(), "d"))
.withMessage("Nested entry name must end with '/'");
}
@Test
void getDataWhenNestedDirectoryReturnsVirtualZipDataBlock() throws IOException {
try (ZipContent nested = ZipContent.open(this.file.toPath(), "d/")) {
File file = new File(this.tempDir, "included.zip");
write(file, nested.openRawZipData());
try (ZipFile loadedZipFile = new ZipFile(file)) {
assertThat(loadedZipFile.size()).isEqualTo(3);
assertThat(loadedZipFile.stream().map(ZipEntry::getName)).containsExactly("META-INF/",
"META-INF/MANIFEST.MF", "9.dat");
assertThat(loadedZipFile.getEntry("9.dat")).isNotNull();
try (InputStream in = loadedZipFile.getInputStream(loadedZipFile.getEntry("9.dat"))) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
in.transferTo(out);
assertThat(out.toByteArray()).containsExactly(0x09);
}
}
}
}
@Test
void loadWhenHasFrontMatterOpensZip() throws IOException {
File fileWithFrontMatter = new File(this.tempDir, "withfrontmatter.jar");
FileOutputStream outputStream = new FileOutputStream(fileWithFrontMatter);
StreamUtils.copy("#/bin/bash", Charset.defaultCharset(), outputStream);
FileCopyUtils.copy(new FileInputStream(this.file), outputStream);
try (ZipContent zip = ZipContent.open(fileWithFrontMatter.toPath())) {
assertThat(zip.size()).isEqualTo(12);
assertThat(zip.getEntry(0).getName()).isEqualTo("META-INF/");
assertThat(zip.getEntry(1).getName()).isEqualTo("META-INF/MANIFEST.MF");
assertThat(zip.getEntry(2).getName()).isEqualTo("1.dat");
assertThat(zip.getEntry(3).getName()).isEqualTo("2.dat");
assertThat(zip.getEntry(4).getName()).isEqualTo("d/");
assertThat(zip.getEntry(5).getName()).isEqualTo("d/9.dat");
assertThat(zip.getEntry(6).getName()).isEqualTo("special/");
assertThat(zip.getEntry(7).getName()).isEqualTo("special/\u00EB.dat");
assertThat(zip.getEntry(8).getName()).isEqualTo("nested.jar");
assertThat(zip.getEntry(9).getName()).isEqualTo("another-nested.jar");
assertThat(zip.getEntry(10).getName()).isEqualTo("space nested.jar");
assertThat(zip.getEntry(11).getName()).isEqualTo("multi-release.jar");
}
}
@Test
void openWhenZip64ThatExceedsZipEntryLimitOpensZip() throws Exception {
File zip64File = new File(this.tempDir, "zip64.zip");
FileCopyUtils.copy(zip64Bytes(), zip64File);
try (ZipContent zip64Content = ZipContent.open(zip64File.toPath())) {
assertThat(zip64Content.size()).isEqualTo(65537);
for (int i = 0; i < zip64Content.size(); i++) {
Entry entry = zip64Content.getEntry(i);
try (CloseableDataBlock dataBlock = entry.openContent()) {
assertThat(asInflaterInputStream(dataBlock)).hasContent("Entry " + (i + 1));
}
}
}
}
@Test
void openWhenZip64ThatExceedsZipSizeLimitOpensZip() throws Exception {
Assumptions.assumeTrue(this.tempDir.getFreeSpace() > 6 * 1024 * 1024 * 1024, "Insufficient disk space");
File zip64File = new File(this.tempDir, "zip64.zip");
File entryFile = new File(this.tempDir, "entry.dat");
CRC32 crc32 = new CRC32();
try (FileOutputStream entryOut = new FileOutputStream(entryFile)) {
byte[] data = new byte[1024 * 1024];
new Random().nextBytes(data);
for (int i = 0; i < 1024; i++) {
entryOut.write(data);
crc32.update(data);
}
}
try (ZipOutputStream zipOutput = new ZipOutputStream(new FileOutputStream(zip64File))) {
for (int i = 0; i < 6; i++) {
ZipEntry storedEntry = new ZipEntry("huge-" + i);
storedEntry.setSize(entryFile.length());
storedEntry.setCompressedSize(entryFile.length());
storedEntry.setCrc(crc32.getValue());
storedEntry.setMethod(ZipEntry.STORED);
zipOutput.putNextEntry(storedEntry);
try (FileInputStream entryIn = new FileInputStream(entryFile)) {
StreamUtils.copy(entryIn, zipOutput);
}
zipOutput.closeEntry();
}
}
try (ZipContent zip64Content = ZipContent.open(zip64File.toPath())) {
assertThat(zip64Content.size()).isEqualTo(6);
}
}
@Test
void nestedZip64CanBeRead() throws Exception {
File containerFile = new File(this.tempDir, "outer.zip");
try (ZipOutputStream jarOutput = new ZipOutputStream(new FileOutputStream(containerFile))) {
ZipEntry nestedEntry = new ZipEntry("nested-zip64.zip");
byte[] contents = zip64Bytes();
nestedEntry.setSize(contents.length);
nestedEntry.setCompressedSize(contents.length);
CRC32 crc32 = new CRC32();
crc32.update(contents);
nestedEntry.setCrc(crc32.getValue());
nestedEntry.setMethod(ZipEntry.STORED);
jarOutput.putNextEntry(nestedEntry);
jarOutput.write(contents);
jarOutput.closeEntry();
}
try (ZipContent nestedZip = ZipContent.open(containerFile.toPath(), "nested-zip64.zip")) {
assertThat(nestedZip.size()).isEqualTo(65537);
for (int i = 0; i < nestedZip.size(); i++) {
Entry entry = nestedZip.getEntry(i);
try (CloseableDataBlock content = entry.openContent()) {
assertThat(asInflaterInputStream(content)).hasContent("Entry " + (i + 1));
}
}
}
}
private byte[] zip64Bytes() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
ZipOutputStream zipOutput = new ZipOutputStream(bytes);
for (int i = 0; i < 65537; i++) {
zipOutput.putNextEntry(new ZipEntry(i + ".dat"));
zipOutput.write(("Entry " + (i + 1)).getBytes(StandardCharsets.UTF_8));
zipOutput.closeEntry();
}
zipOutput.close();
return bytes.toByteArray();
}
@Test
void entryWithEpochTimeOfZeroShouldNotFail() throws Exception {
File file = createZipFileWithEpochTimeOfZero();
try (ZipContent zip = ZipContent.open(file.toPath())) {
ZipEntry entry = zip.getEntry(0).as(ZipEntry::new);
assertThat(entry.getLastModifiedTime().toInstant()).isEqualTo(Instant.EPOCH);
assertThat(entry.getName()).isEqualTo("1.dat");
}
}
private File createZipFileWithEpochTimeOfZero() throws Exception {
File file = new File(this.tempDir, "temp.zip");
String comment = "outer";
try (ZipOutputStream zipOutput = new ZipOutputStream(new FileOutputStream(file))) {
zipOutput.setComment(comment);
ZipEntry entry = new ZipEntry("1.dat");
entry.setLastModifiedTime(FileTime.from(Instant.EPOCH));
zipOutput.putNextEntry(entry);
zipOutput.write(new byte[] { (byte) 1 });
zipOutput.closeEntry();
}
ByteBuffer data = ByteBuffer.wrap(Files.readAllBytes(file.toPath()));
data.order(ByteOrder.LITTLE_ENDIAN);
int endOfCentralDirectoryRecordPos = data.remaining() - ZipFile.ENDHDR - comment.getBytes().length;
data.position(endOfCentralDirectoryRecordPos + ZipFile.ENDOFF);
int startOfCentralDirectoryOffset = data.getInt();
data.position(startOfCentralDirectoryOffset + ZipFile.CENOFF);
int localHeaderPosition = data.getInt();
writeTimeBlock(data.array(), startOfCentralDirectoryOffset + ZipFile.CENTIM, 0);
writeTimeBlock(data.array(), localHeaderPosition + ZipFile.LOCTIM, 0);
File zerotimedFile = new File(this.tempDir, "zerotimed.zip");
Files.write(zerotimedFile.toPath(), data.array());
return zerotimedFile;
}
@Test
void getInfoReturnsComputedInfo() {
ZipInfo info = this.zipContent.getInfo(ZipInfo.class, ZipInfo::get);
assertThat(info.size()).isEqualTo(12);
}
private static void writeTimeBlock(byte[] data, int pos, int value) {
data[pos] = (byte) (value & 0xff);
data[pos + 1] = (byte) ((value >> 8) & 0xff);
data[pos + 2] = (byte) ((value >> 16) & 0xff);
data[pos + 3] = (byte) ((value >> 24) & 0xff);
}
private InputStream asInflaterInputStream(DataBlock dataBlock) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate((int) dataBlock.size() + 1);
buffer.limit(buffer.limit() - 1);
dataBlock.readFully(buffer, 0);
ByteArrayInputStream in = new ByteArrayInputStream(buffer.array());
return new InflaterInputStream(in, new Inflater(true));
}
private void write(File file, CloseableDataBlock dataBlock) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate((int) dataBlock.size());
dataBlock.readFully(buffer, 0);
Files.write(file.toPath(), buffer.array());
dataBlock.close();
}
private static class ZipInfo {
private int size;
ZipInfo(int size) {
this.size = size;
}
int size() {
return this.size;
}
static ZipInfo get(ZipContent content) {
return new ZipInfo(content.size());
}
}
}

View File

@@ -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 org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIOException;
/**
* Tests for {@link ZipEndOfCentralDirectoryRecord}.
*
* @author Phillip Webb
*/
class ZipEndOfCentralDirectoryRecordTests {
@Test
void loadLocatesAndLoadsData() throws Exception {
DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { //
0x50, 0x4b, 0x05, 0x06, //
0x01, 0x00, //
0x02, 0x00, //
0x03, 0x00, //
0x04, 0x00, //
0x05, 0x00, 0x00, 0x00, //
0x06, 0x00, 0x00, 0x00, //
0x07, 0x00 }); //
ZipEndOfCentralDirectoryRecord.Located located = ZipEndOfCentralDirectoryRecord.load(dataBlock);
assertThat(located.pos()).isEqualTo(0L);
ZipEndOfCentralDirectoryRecord record = located.endOfCentralDirectoryRecord();
assertThat(record.numberOfThisDisk()).isEqualTo((short) 1);
assertThat(record.diskWhereCentralDirectoryStarts()).isEqualTo((short) 2);
assertThat(record.numberOfCentralDirectoryEntriesOnThisDisk()).isEqualTo((short) 3);
assertThat(record.totalNumberOfCentralDirectoryEntries()).isEqualTo((short) 4);
assertThat(record.sizeOfCentralDirectory()).isEqualTo(5);
assertThat(record.offsetToStartOfCentralDirectory()).isEqualTo(6);
assertThat(record.commentLength()).isEqualTo((short) 7);
}
@Test
void loadWhenMultipleBuffersBackLoadsData() throws Exception {
byte[] bytes = new byte[ZipEndOfCentralDirectoryRecord.BUFFER_SIZE * 4];
byte[] data = new byte[] { //
0x50, 0x4b, 0x05, 0x06, //
0x01, 0x00, //
0x02, 0x00, //
0x03, 0x00, //
0x04, 0x00, //
0x05, 0x00, 0x00, 0x00, //
0x06, 0x00, 0x00, 0x00, //
0x07, 0x00 }; //
System.arraycopy(data, 0, bytes, 4, data.length);
ZipEndOfCentralDirectoryRecord.Located located = ZipEndOfCentralDirectoryRecord
.load(new ByteArrayDataBlock(bytes));
assertThat(located.pos()).isEqualTo(4L);
}
@Test
void loadWhenSignatureDoesNotMatchThrowsException() {
DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { //
0x51, 0x4b, 0x05, 0x06, //
0x01, 0x00, //
0x02, 0x00, //
0x03, 0x00, //
0x04, 0x00, //
0x05, 0x00, 0x00, 0x00, //
0x06, 0x00, 0x00, 0x00, //
0x07, 0x00 }); //
assertThatIOException().isThrownBy(() -> ZipEndOfCentralDirectoryRecord.load(dataBlock))
.withMessageContaining("'End Of Central Directory Record' not found");
}
@Test
void asByteArrayReturnsByteArray() throws Exception {
byte[] bytes = new byte[] { //
0x50, 0x4b, 0x05, 0x06, //
0x01, 0x00, //
0x02, 0x00, //
0x03, 0x00, //
0x04, 0x00, //
0x05, 0x00, 0x00, 0x00, //
0x06, 0x00, 0x00, 0x00, //
0x07, 0x00 }; //
ZipEndOfCentralDirectoryRecord.Located located = ZipEndOfCentralDirectoryRecord
.load(new ByteArrayDataBlock(bytes));
assertThat(located.endOfCentralDirectoryRecord().asByteArray()).isEqualTo(bytes);
}
@Test
void sizeReturnsSize() {
ZipEndOfCentralDirectoryRecord record = new ZipEndOfCentralDirectoryRecord((short) 1, (short) 2, (short) 3,
(short) 4, 5, 6, (short) 7);
assertThat(record.size()).isEqualTo(29L);
}
}

View File

@@ -0,0 +1,117 @@
/*
* 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 org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIOException;
/**
* Tests for {@link ZipLocalFileHeaderRecord}.
*
* @author Phillip Webb
*/
class ZipLocalFileHeaderRecordTests {
@Test
void loadLoadsData() throws Exception {
DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { //
0x50, 0x4b, 0x03, 0x04, //
0x01, 0x00, //
0x02, 0x00, //
0x03, 0x00, //
0x04, 0x00, //
0x05, 0x00, //
0x06, 0x00, 0x00, 0x00, //
0x07, 0x00, 0x00, 0x00, //
0x08, 0x00, 0x00, 0x00, //
0x09, 0x00, //
0x0A, 0x00 }); //
ZipLocalFileHeaderRecord record = ZipLocalFileHeaderRecord.load(dataBlock, 0);
assertThat(record.versionNeededToExtract()).isEqualTo((short) 1);
assertThat(record.generalPurposeBitFlag()).isEqualTo((short) 2);
assertThat(record.compressionMethod()).isEqualTo((short) 3);
assertThat(record.lastModFileTime()).isEqualTo((short) 4);
assertThat(record.lastModFileDate()).isEqualTo((short) 5);
assertThat(record.crc32()).isEqualTo(6);
assertThat(record.compressedSize()).isEqualTo(7);
assertThat(record.uncompressedSize()).isEqualTo(8);
assertThat(record.fileNameLength()).isEqualTo((short) 9);
assertThat(record.extraFieldLength()).isEqualTo((short) 10);
}
@Test
void loadWhenSignatureDoesNotMatchThrowsException() {
DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { //
0x51, 0x4b, 0x03, 0x04, //
0x01, 0x00, //
0x02, 0x00, //
0x03, 0x00, //
0x04, 0x00, //
0x05, 0x00, //
0x06, 0x00, 0x00, 0x00, //
0x07, 0x00, 0x00, 0x00, //
0x08, 0x00, 0x00, 0x00, //
0x09, 0x00, //
0x0A, 0x00 }); //
assertThatIOException().isThrownBy(() -> ZipLocalFileHeaderRecord.load(dataBlock, 0))
.withMessageContaining("'Local File Header Record' not found");
}
@Test
void sizeReturnsSize() {
ZipLocalFileHeaderRecord record = new ZipLocalFileHeaderRecord((short) 1, (short) 2, (short) 3, (short) 4,
(short) 5, 6, 7, 8, (short) 9, (short) 10);
assertThat(record.size()).isEqualTo(49L);
}
@Test
void withExtraFieldLengthReturnsUpdatedInstance() {
ZipLocalFileHeaderRecord record = new ZipLocalFileHeaderRecord((short) 1, (short) 2, (short) 3, (short) 4,
(short) 5, 6, 7, 8, (short) 9, (short) 10)
.withExtraFieldLength((short) 100);
assertThat(record.extraFieldLength()).isEqualTo((short) 100);
}
@Test
void withFileNameLengthReturnsUpdatedInstance() {
ZipLocalFileHeaderRecord record = new ZipLocalFileHeaderRecord((short) 1, (short) 2, (short) 3, (short) 4,
(short) 5, 6, 7, 8, (short) 9, (short) 10)
.withFileNameLength((short) 100);
assertThat(record.fileNameLength()).isEqualTo((short) 100);
}
@Test
void asByteArrayReturnsByteArray() throws Exception {
byte[] bytes = new byte[] { //
0x50, 0x4b, 0x03, 0x04, //
0x01, 0x00, //
0x02, 0x00, //
0x03, 0x00, //
0x04, 0x00, //
0x05, 0x00, //
0x06, 0x00, 0x00, 0x00, //
0x07, 0x00, 0x00, 0x00, //
0x08, 0x00, 0x00, 0x00, //
0x09, 0x00, //
0x0A, 0x00 }; //
ZipLocalFileHeaderRecord record = ZipLocalFileHeaderRecord.load(new ByteArrayDataBlock(bytes), 0);
assertThat(record.asByteArray()).isEqualTo(bytes);
}
}

View File

@@ -0,0 +1,194 @@
/*
* 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.charset.StandardCharsets;
import org.assertj.core.api.AbstractBooleanAssert;
import org.assertj.core.api.AbstractIntegerAssert;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ZipString}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class ZipStringTests {
@ParameterizedTest
@EnumSource
void hashGeneratesCorrectHashCode(HashSourceType sourceType) throws Exception {
testHash(sourceType, true, "abcABC123xyz!");
testHash(sourceType, false, "abcABC123xyz!");
}
@ParameterizedTest
@EnumSource
void hashWhenHasSpecialCharsGeneratesCorrectHashCode(HashSourceType sourceType) throws Exception {
testHash(sourceType, true, "special/\u00EB.dat");
}
@ParameterizedTest
@EnumSource
void hashWhenHasCyrillicCharsGeneratesCorrectHashCode(HashSourceType sourceType) throws Exception {
testHash(sourceType, true, "\u0432\u0435\u0441\u043D\u0430");
}
@ParameterizedTest
@EnumSource
void hashWhenHasEmojiGeneratesCorrectHashCode(HashSourceType sourceType) throws Exception {
testHash(sourceType, true, "\ud83d\udca9");
}
@ParameterizedTest
@EnumSource
void hashWhenOnlyDifferenceIsEndSlashGeneratesSameHashCode(HashSourceType sourceType) throws Exception {
testHash(sourceType, "", true, "/".hashCode());
testHash(sourceType, "/", true, "/".hashCode());
testHash(sourceType, "a/b", true, "a/b/".hashCode());
testHash(sourceType, "a/b/", true, "a/b/".hashCode());
}
void testHash(HashSourceType sourceType, boolean addSlash, String source) throws Exception {
String expected = (addSlash && !source.endsWith("/")) ? source + "/" : source;
testHash(sourceType, source, addSlash, expected.hashCode());
}
void testHash(HashSourceType sourceType, String source, boolean addEndSlash, int expected) throws Exception {
switch (sourceType) {
case STRING -> {
assertThat(ZipString.hash(source, addEndSlash)).isEqualTo(expected);
}
case CHAR_SEQUENCE -> {
CharSequence charSequence = new StringBuilder(source);
assertThat(ZipString.hash(charSequence, addEndSlash)).isEqualTo(expected);
}
case DATA_BLOCK -> {
ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(source.getBytes(StandardCharsets.UTF_8));
assertThat(ZipString.hash(null, dataBlock, 0, (int) dataBlock.size(), addEndSlash)).isEqualTo(expected);
}
}
}
@Test
void matchesWhenExactMatchReturnsTrue() throws Exception {
assertMatches("one/two/three", "one/two/three", false).isTrue();
}
@Test
void matchesWhenNotMatchWithSameLengthReturnsFalse() throws Exception {
assertMatches("one/two/three", "one/too/three", false).isFalse();
}
@Test
void matchesWhenExactMatchWithSpecialCharsReturnsTrue() throws Exception {
assertMatches("special/\u00EB.dat", "special/\u00EB.dat", false).isTrue();
}
@Test
void matchesWhenExactMatchWithCyrillicCharsReturnsTrue() throws Exception {
assertMatches("\u0432\u0435\u0441\u043D\u0430", "\u0432\u0435\u0441\u043D\u0430", false).isTrue();
}
@Test
void matchesWhenNoMatchWithCyrillicCharsReturnsFalse() throws Exception {
assertMatches("\u0432\u0435\u0441\u043D\u0430", "\u0432\u0435\u0441\u043D\u043D", false).isFalse();
}
@Test
void matchesWhenExactMatchWithEmojiCharsReturnsTrue() throws Exception {
assertMatches("\ud83d\udca9", "\ud83d\udca9", false).isTrue();
}
@Test
void matchesWithAddSlash() throws Exception {
assertMatches("META-INF/MANFIFEST.MF", "META-INF/MANFIFEST.MF", true).isTrue();
assertMatches("one/two/three/", "one/two/three", true).isTrue();
assertMatches("one/two/three", "one/two/three/", true).isFalse();
assertMatches("one/two/three/", "one/too/three", true).isFalse();
assertMatches("one/two/three", "one/too/three/", true).isFalse();
assertMatches("one/two/three//", "one/two/three", true).isFalse();
assertMatches("one/two/three", "one/two/three//", true).isFalse();
}
@Test
void matchesWhenDataBlockShorterThenCharSequenceReturnsFalse() throws Exception {
assertMatches("one/two/thre", "one/two/three", false).isFalse();
}
@Test
void matchesWhenCharSequenceShorterThanDataBlockReturnsFalse() throws Exception {
assertMatches("one/two/three", "one/two/thre", false).isFalse();
}
@Test
void startsWithWhenStartsWith() throws Exception {
assertStartsWith("one/two", "one/").isEqualTo(4);
}
@Test
void startsWithWhenExact() throws Exception {
assertStartsWith("one/", "one/").isEqualTo(4);
}
@Test
void startsWithWhenTooShort() throws Exception {
assertStartsWith("one/two", "one/two/three/").isEqualTo(-1);
}
@Test
void startsWithWhenDoesNotStartWith() throws Exception {
assertStartsWith("one/three/", "one/two/").isEqualTo(-1);
}
@Test
void zipStringWhenMultiCodePointAtBufferBoundary() throws Exception {
StringBuilder source = new StringBuilder();
for (int i = 0; i < ZipString.BUFFER_SIZE - 1; i++) {
source.append("A");
}
source.append("\u1EFF");
String charSequence = source.toString();
source.append("suffix");
assertStartsWith(source.toString(), charSequence);
}
private AbstractBooleanAssert<?> assertMatches(String source, CharSequence charSequence, boolean addSlash)
throws Exception {
ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(source.getBytes(StandardCharsets.UTF_8));
return assertThat(ZipString.matches(null, dataBlock, 0, (int) dataBlock.size(), charSequence, addSlash));
}
private AbstractIntegerAssert<?> assertStartsWith(String source, CharSequence charSequence) throws IOException {
ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(source.getBytes(StandardCharsets.UTF_8));
return assertThat(ZipString.startsWith(null, dataBlock, 0, (int) dataBlock.size(), charSequence));
}
enum HashSourceType {
STRING, CHAR_SEQUENCE, DATA_BLOCK
}
}