diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java index 4a3aa0b28c..8d4c0e9dbc 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,11 @@ package org.springframework.boot.loader.jar; import java.io.IOException; -import java.time.LocalDateTime; import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.time.temporal.ValueRange; import org.springframework.boot.loader.data.RandomAccessData; @@ -27,6 +30,7 @@ import org.springframework.boot.loader.data.RandomAccessData; * * @author Phillip Webb * @author Andy Wilkinson + * @author Dmytro Nosan * @see Zip File Format */ @@ -124,10 +128,14 @@ final class CentralDirectoryFileHeader implements FileHeader { * @return the date and time as milliseconds since the epoch */ private long decodeMsDosFormatDateTime(long datetime) { - LocalDateTime localDateTime = LocalDateTime.of((int) (((datetime >> 25) & 0x7f) + 1980), - (int) ((datetime >> 21) & 0x0f), (int) ((datetime >> 16) & 0x1f), (int) ((datetime >> 11) & 0x1f), - (int) ((datetime >> 5) & 0x3f), (int) ((datetime << 1) & 0x3e)); - return localDateTime.toEpochSecond(ZoneId.systemDefault().getRules().getOffset(localDateTime)) * 1000; + int year = getChronoValue(((datetime >> 25) & 0x7f) + 1980, ChronoField.YEAR); + int month = getChronoValue((datetime >> 21) & 0x0f, ChronoField.MONTH_OF_YEAR); + int day = getChronoValue((datetime >> 16) & 0x1f, ChronoField.DAY_OF_MONTH); + int hour = getChronoValue((datetime >> 11) & 0x1f, ChronoField.HOUR_OF_DAY); + int minute = getChronoValue((datetime >> 5) & 0x3f, ChronoField.MINUTE_OF_HOUR); + int second = getChronoValue((datetime << 1) & 0x3e, ChronoField.SECOND_OF_MINUTE); + return ZonedDateTime.of(year, month, day, hour, minute, second, 0, ZoneId.systemDefault()).toInstant() + .truncatedTo(ChronoUnit.SECONDS).toEpochMilli(); } long getCrc() { @@ -176,4 +184,9 @@ final class CentralDirectoryFileHeader implements FileHeader { return fileHeader; } + private static int getChronoValue(long value, ChronoField field) { + ValueRange range = field.range(); + return Math.toIntExact(Math.min(Math.max(value, range.getMinimum()), range.getMaximum())); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java index 9ec200da7e..3b29fd2290 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java @@ -28,6 +28,8 @@ import java.net.URL; import java.net.URLClassLoader; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.attribute.FileTime; +import java.time.Instant; import java.util.Collections; import java.util.Enumeration; import java.util.List; @@ -47,6 +49,7 @@ import org.junit.jupiter.api.io.TempDir; import org.springframework.boot.loader.TestJarCreator; import org.springframework.boot.loader.data.RandomAccessDataFile; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.FileCopyUtils; import org.springframework.util.StreamUtils; @@ -62,6 +65,7 @@ import static org.mockito.Mockito.verify; * @author Phillip Webb * @author Martin Lau * @author Andy Wilkinson + * @author Madhura Bhave */ @ExtendWith(JarUrlProtocolHandler.class) class JarFileTests { @@ -578,6 +582,26 @@ class JarFileTests { return bytes.toByteArray(); } + @Test + void jarFileEntryWithEpochTimeOfZeroShouldNotFail() throws Exception { + File file = new File(this.tempDir, "timed.jar"); + FileOutputStream fileOutputStream = new FileOutputStream(file); + try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { + jarOutputStream.setComment("outer"); + JarEntry entry = new JarEntry("1.dat"); + entry.setLastModifiedTime(FileTime.from(Instant.EPOCH)); + ReflectionTestUtils.setField(entry, "xdostime", 0); + jarOutputStream.putNextEntry(entry); + jarOutputStream.write(new byte[] { (byte) 1 }); + jarOutputStream.closeEntry(); + } + JarFile jarFile = new JarFile(file); + Enumeration entries = jarFile.entries(); + JarEntry entry = entries.nextElement(); + assertThat(entry.getLastModifiedTime().toInstant()).isEqualTo(Instant.EPOCH); + assertThat(entry.getName()).isEqualTo("1.dat"); + } + private int getJavaVersion() { try { Object runtimeVersion = Runtime.class.getMethod("version").invoke(null);