Skip to content

Commit 558d811

Browse files
committed
Use JarFile instead of ZipInputStream
ZipInputStream can't cope with some non-deflated entries, see https://bugs.openjdk.org/browse/JDK-8143613. JarFile works better, but it doesn't support creation time / access time. See gh-38276
1 parent 64e4738 commit 558d811

File tree

3 files changed

+61
-90
lines changed

3 files changed

+61
-90
lines changed

spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java

+56-30
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,20 @@
2020
import java.io.FileInputStream;
2121
import java.io.FileOutputStream;
2222
import java.io.IOException;
23+
import java.io.InputStream;
2324
import java.io.OutputStream;
2425
import java.io.PrintStream;
2526
import java.io.UncheckedIOException;
2627
import java.nio.file.Files;
2728
import java.nio.file.attribute.BasicFileAttributeView;
2829
import java.nio.file.attribute.FileTime;
30+
import java.util.Enumeration;
2931
import java.util.List;
3032
import java.util.Locale;
3133
import java.util.Map;
3234
import java.util.Set;
3335
import java.util.jar.JarEntry;
36+
import java.util.jar.JarFile;
3437
import java.util.jar.JarOutputStream;
3538
import java.util.jar.Manifest;
3639
import java.util.zip.ZipEntry;
@@ -157,8 +160,8 @@ private void printError(PrintStream out, String message) {
157160
private void extractLibraries(FileResolver fileResolver, JarStructure jarStructure, Map<Option, String> options)
158161
throws IOException {
159162
String librariesDirectory = getLibrariesDirectory(options);
160-
extractArchive(fileResolver, (zipEntry) -> {
161-
Entry entry = jarStructure.resolve(zipEntry);
163+
extractArchive(fileResolver, (jarEntry) -> {
164+
Entry entry = jarStructure.resolve(jarEntry);
162165
if (isType(entry, Type.LIBRARY)) {
163166
return librariesDirectory + entry.location();
164167
}
@@ -212,22 +215,22 @@ private JarStructure getJarStructure() {
212215
}
213216

214217
private void extractArchive(FileResolver fileResolver) throws IOException {
215-
extractArchive(fileResolver, ZipEntry::getName);
218+
extractArchive(fileResolver, JarEntry::getName);
216219
}
217220

218221
private void extractArchive(FileResolver fileResolver, EntryNameTransformer entryNameTransformer)
219222
throws IOException {
220-
withZipEntries(this.context.getArchiveFile(), (stream, zipEntry) -> {
221-
if (zipEntry.isDirectory()) {
223+
withJarEntries(this.context.getArchiveFile(), (stream, jarEntry) -> {
224+
if (jarEntry.isDirectory()) {
222225
return;
223226
}
224-
String name = entryNameTransformer.getName(zipEntry);
227+
String name = entryNameTransformer.getName(jarEntry);
225228
if (name == null) {
226229
return;
227230
}
228-
File file = fileResolver.resolve(zipEntry, name);
231+
File file = fileResolver.resolve(jarEntry, name);
229232
if (file != null) {
230-
extractEntry(stream, zipEntry, file);
233+
extractEntry(stream, jarEntry, file);
231234
}
232235
});
233236
}
@@ -249,11 +252,11 @@ private void createApplication(JarStructure jarStructure, FileResolver fileResol
249252
Manifest manifest = jarStructure.createLauncherManifest((library) -> librariesDirectory + library);
250253
mkDirs(file.getParentFile());
251254
try (JarOutputStream output = new JarOutputStream(new FileOutputStream(file), manifest)) {
252-
withZipEntries(this.context.getArchiveFile(), ((stream, zipEntry) -> {
253-
Entry entry = jarStructure.resolve(zipEntry);
255+
withJarEntries(this.context.getArchiveFile(), ((stream, jarEntry) -> {
256+
Entry entry = jarStructure.resolve(jarEntry);
254257
if (isType(entry, Type.APPLICATION_CLASS_OR_RESOURCE) && StringUtils.hasLength(entry.location())) {
255-
JarEntry jarEntry = createJarEntry(entry.location(), zipEntry);
256-
output.putNextEntry(jarEntry);
258+
JarEntry newJarEntry = createJarEntry(entry.location(), jarEntry);
259+
output.putNextEntry(newJarEntry);
257260
StreamUtils.copy(stream, output);
258261
output.closeEntry();
259262
}
@@ -275,51 +278,74 @@ private static boolean isType(Entry entry, Type type) {
275278
return entry.type() == type;
276279
}
277280

278-
private static void extractEntry(ZipInputStream zip, ZipEntry entry, File file) throws IOException {
281+
private static void extractEntry(InputStream stream, JarEntry entry, File file) throws IOException {
279282
mkDirs(file.getParentFile());
280283
try (OutputStream out = new FileOutputStream(file)) {
281-
StreamUtils.copy(zip, out);
284+
StreamUtils.copy(stream, out);
282285
}
283286
try {
284287
Files.getFileAttributeView(file.toPath(), BasicFileAttributeView.class)
285-
.setTimes(entry.getLastModifiedTime(), entry.getLastAccessTime(), entry.getCreationTime());
288+
.setTimes(getLastModifiedTime(entry), getLastAccessTime(entry), getCreationTime(entry));
286289
}
287290
catch (IOException ex) {
288291
// File system does not support setting time attributes. Continue.
289292
}
290293
}
291294

295+
private static FileTime getCreationTime(JarEntry entry) {
296+
if (entry.getCreationTime() != null) {
297+
return entry.getCreationTime();
298+
}
299+
return entry.getLastModifiedTime();
300+
}
301+
302+
private static FileTime getLastAccessTime(JarEntry entry) {
303+
if (entry.getLastAccessTime() != null) {
304+
return entry.getLastAccessTime();
305+
}
306+
return getLastModifiedTime(entry);
307+
}
308+
309+
private static FileTime getLastModifiedTime(JarEntry entry) {
310+
if (entry.getLastModifiedTime() != null) {
311+
return entry.getLastModifiedTime();
312+
}
313+
return entry.getCreationTime();
314+
}
315+
292316
private static void mkDirs(File file) throws IOException {
293317
if (!file.exists() && !file.mkdirs()) {
294318
throw new IOException("Unable to create directory " + file);
295319
}
296320
}
297321

298-
private static JarEntry createJarEntry(String location, ZipEntry originalEntry) {
322+
private static JarEntry createJarEntry(String location, JarEntry originalEntry) {
299323
JarEntry jarEntry = new JarEntry(location);
300-
FileTime lastModifiedTime = originalEntry.getLastModifiedTime();
324+
FileTime lastModifiedTime = getLastModifiedTime(originalEntry);
301325
if (lastModifiedTime != null) {
302326
jarEntry.setLastModifiedTime(lastModifiedTime);
303327
}
304-
FileTime lastAccessTime = originalEntry.getLastAccessTime();
328+
FileTime lastAccessTime = getLastAccessTime(originalEntry);
305329
if (lastAccessTime != null) {
306330
jarEntry.setLastAccessTime(lastAccessTime);
307331
}
308-
FileTime creationTime = originalEntry.getCreationTime();
332+
FileTime creationTime = getCreationTime(originalEntry);
309333
if (creationTime != null) {
310334
jarEntry.setCreationTime(creationTime);
311335
}
312336
return jarEntry;
313337
}
314338

315-
private static void withZipEntries(File file, ThrowingConsumer callback) throws IOException {
316-
try (ZipInputStream stream = new ZipInputStream(new FileInputStream(file))) {
317-
ZipEntry entry = stream.getNextEntry();
318-
while (entry != null) {
339+
private static void withJarEntries(File file, ThrowingConsumer callback) throws IOException {
340+
try (JarFile jarFile = new JarFile(file)) {
341+
Enumeration<JarEntry> entries = jarFile.entries();
342+
while (entries.hasMoreElements()) {
343+
JarEntry entry = entries.nextElement();
319344
if (StringUtils.hasLength(entry.getName())) {
320-
callback.accept(stream, entry);
345+
try (InputStream stream = jarFile.getInputStream(entry)) {
346+
callback.accept(stream, entry);
347+
}
321348
}
322-
entry = stream.getNextEntry();
323349
}
324350
}
325351
}
@@ -336,14 +362,14 @@ private static File assertFileIsContainedInDirectory(File directory, File file,
336362
@FunctionalInterface
337363
private interface EntryNameTransformer {
338364

339-
String getName(ZipEntry entry);
365+
String getName(JarEntry entry);
340366

341367
}
342368

343369
@FunctionalInterface
344370
private interface ThrowingConsumer {
345371

346-
void accept(ZipInputStream stream, ZipEntry entry) throws IOException;
372+
void accept(InputStream stream, JarEntry entry) throws IOException;
347373

348374
}
349375

@@ -356,14 +382,14 @@ private interface FileResolver {
356382
void createDirectories() throws IOException;
357383

358384
/**
359-
* Resolves the given {@link ZipEntry} to a file.
360-
* @param entry the zip entry
385+
* Resolves the given {@link JarEntry} to a file.
386+
* @param entry the jar entry
361387
* @param newName the new name of the file
362388
* @return file where the contents should be written or {@code null} if this entry
363389
* should be skipped
364390
* @throws IOException if something went wrong
365391
*/
366-
default File resolve(ZipEntry entry, String newName) throws IOException {
392+
default File resolve(JarEntry entry, String newName) throws IOException {
367393
return resolve(entry.getName(), newName);
368394
}
369395

spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java

+5-33
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,18 @@
1919
import java.io.File;
2020
import java.io.FileWriter;
2121
import java.io.IOException;
22-
import java.lang.Runtime.Version;
2322
import java.nio.file.Files;
2423
import java.nio.file.attribute.BasicFileAttributeView;
2524
import java.nio.file.attribute.BasicFileAttributes;
2625
import java.time.Instant;
2726
import java.time.temporal.ChronoUnit;
28-
import java.util.EnumSet;
2927
import java.util.List;
3028
import java.util.Map;
3129
import java.util.jar.Manifest;
3230

3331
import org.junit.jupiter.api.BeforeEach;
3432
import org.junit.jupiter.api.Nested;
3533
import org.junit.jupiter.api.Test;
36-
import org.junit.jupiter.api.condition.JRE;
37-
import org.junit.jupiter.api.condition.OS;
3834

3935
import static org.assertj.core.api.Assertions.assertThat;
4036
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
@@ -46,13 +42,11 @@
4642
*/
4743
class ExtractCommandTests extends AbstractTests {
4844

49-
private static final Instant NOW = Instant.now();
45+
private static final Instant CREATION_TIME = Instant.parse("2020-01-01T00:00:00Z");
5046

51-
private static final Instant CREATION_TIME = NOW.minus(3, ChronoUnit.DAYS);
47+
private static final Instant LAST_MODIFIED_TIME = Instant.parse("2021-01-01T00:00:00Z");
5248

53-
private static final Instant LAST_MODIFIED_TIME = NOW.minus(2, ChronoUnit.DAYS);
54-
55-
private static final Instant LAST_ACCESS_TIME = NOW.minus(1, ChronoUnit.DAYS);
49+
private static final Instant LAST_ACCESS_TIME = Instant.parse("2022-01-01T00:00:00Z");
5650

5751
private File archive;
5852

@@ -85,35 +79,13 @@ private void timeAttributes(File file) {
8579
.getFileAttributeView(file.toPath(), BasicFileAttributeView.class)
8680
.readAttributes();
8781
assertThat(basicAttributes.lastModifiedTime().toInstant().truncatedTo(ChronoUnit.SECONDS))
82+
.as("last modified time")
8883
.isEqualTo(LAST_MODIFIED_TIME.truncatedTo(ChronoUnit.SECONDS));
89-
Instant expectedCreationTime = expectedCreationTime();
90-
if (expectedCreationTime != null) {
91-
assertThat(basicAttributes.creationTime().toInstant().truncatedTo(ChronoUnit.SECONDS))
92-
.isEqualTo(expectedCreationTime.truncatedTo(ChronoUnit.SECONDS));
93-
}
94-
assertThat(basicAttributes.lastAccessTime().toInstant().truncatedTo(ChronoUnit.SECONDS))
95-
.isEqualTo(LAST_ACCESS_TIME.truncatedTo(ChronoUnit.SECONDS));
9684
}
9785
catch (IOException ex) {
9886
throw new RuntimeException(ex);
9987
}
100-
}
101-
102-
private Instant expectedCreationTime() {
103-
// macOS uses last modified time until Java 20 where it uses creation time.
104-
// https://github.com/openjdk/jdk21u-dev/commit/6397d564a5dab07f81bf4c69b116ebfabb2446ba
105-
if (OS.MAC.isCurrentOs()) {
106-
return (EnumSet.range(JRE.JAVA_17, JRE.JAVA_19).contains(JRE.currentVersion())) ? LAST_MODIFIED_TIME
107-
: CREATION_TIME;
108-
}
109-
if (OS.LINUX.isCurrentOs()) {
110-
// Linux uses the modified time until Java 21.0.2 where a bug means that it
111-
// uses the birth time which it has not set, preventing us from verifying it.
112-
// https://github.com/openjdk/jdk21u-dev/commit/4cf572e3b99b675418e456e7815fb6fd79245e30
113-
return (Runtime.version().compareTo(Version.parse("21.0.2")) >= 0) ? null : LAST_MODIFIED_TIME;
114-
}
115-
return CREATION_TIME;
116-
}
88+
};
11789

11890
@Nested
11991
class Extract {

spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java

-27
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.io.FileWriter;
2222
import java.io.IOException;
2323
import java.io.InputStreamReader;
24-
import java.lang.Runtime.Version;
2524
import java.nio.file.Files;
2625
import java.nio.file.attribute.BasicFileAttributeView;
2726
import java.nio.file.attribute.BasicFileAttributes;
@@ -30,7 +29,6 @@
3029
import java.time.temporal.ChronoUnit;
3130
import java.util.Arrays;
3231
import java.util.Collections;
33-
import java.util.EnumSet;
3432
import java.util.Iterator;
3533
import java.util.concurrent.TimeUnit;
3634
import java.util.function.Consumer;
@@ -39,8 +37,6 @@
3937

4038
import org.junit.jupiter.api.BeforeEach;
4139
import org.junit.jupiter.api.Test;
42-
import org.junit.jupiter.api.condition.JRE;
43-
import org.junit.jupiter.api.condition.OS;
4440
import org.junit.jupiter.api.extension.ExtendWith;
4541
import org.junit.jupiter.api.io.TempDir;
4642
import org.mockito.Mock;
@@ -112,35 +108,12 @@ private void timeAttributes(File file) {
112108
.readAttributes();
113109
assertThat(basicAttributes.lastModifiedTime().to(TimeUnit.SECONDS))
114110
.isEqualTo(LAST_MODIFIED_TIME.to(TimeUnit.SECONDS));
115-
FileTime expectedCreationTime = expectedCreationTime();
116-
if (expectedCreationTime != null) {
117-
assertThat(basicAttributes.creationTime().to(TimeUnit.SECONDS))
118-
.isEqualTo(expectedCreationTime.to(TimeUnit.SECONDS));
119-
}
120-
assertThat(basicAttributes.lastAccessTime().to(TimeUnit.SECONDS))
121-
.isEqualTo(LAST_ACCESS_TIME.to(TimeUnit.SECONDS));
122111
}
123112
catch (IOException ex) {
124113
throw new RuntimeException(ex);
125114
}
126115
}
127116

128-
private FileTime expectedCreationTime() {
129-
// macOS uses last modified time until Java 20 where it uses creation time.
130-
// https://github.com/openjdk/jdk21u-dev/commit/6397d564a5dab07f81bf4c69b116ebfabb2446ba
131-
if (OS.MAC.isCurrentOs()) {
132-
return (EnumSet.range(JRE.JAVA_17, JRE.JAVA_19).contains(JRE.currentVersion())) ? LAST_MODIFIED_TIME
133-
: CREATION_TIME;
134-
}
135-
if (OS.LINUX.isCurrentOs()) {
136-
// Linux uses the modified time until Java 21.0.2 where a bug means that it
137-
// uses the birth time which it has not set, preventing us from verifying it.
138-
// https://github.com/openjdk/jdk21u-dev/commit/4cf572e3b99b675418e456e7815fb6fd79245e30
139-
return (Runtime.version().compareTo(Version.parse("21.0.2")) >= 0) ? null : LAST_MODIFIED_TIME;
140-
}
141-
return CREATION_TIME;
142-
}
143-
144117
@Test
145118
void runWhenHasDestinationOptionExtractsLayers() {
146119
given(this.context.getArchiveFile()).willReturn(this.jarFile);

0 commit comments

Comments
 (0)