diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java index 508ef2af32..6075218160 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java @@ -22,9 +22,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; -import java.util.Calendar; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.Collection; -import java.util.GregorianCalendar; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -73,8 +73,9 @@ import org.springframework.util.StringUtils; */ class BootZipCopyAction implements CopyAction { - static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = new GregorianCalendar(1980, Calendar.FEBRUARY, 1, 0, 0, 0) - .getTimeInMillis(); + static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = OffsetDateTime.of(1980, 2, 1, 0, 0, 0, 0, ZoneOffset.UTC) + .toInstant() + .toEpochMilli(); private static final Pattern REACHABILITY_METADATA_PROPERTIES_LOCATION_PATTERN = Pattern .compile(ReachabilityMetadataProperties.REACHABILITY_METADATA_PROPERTIES_LOCATION_TEMPLATE.formatted(".*", ".*", @@ -407,7 +408,7 @@ class BootZipCopyAction implements CopyAction { writeParentDirectoriesIfNecessary(name, time); entry.setUnixMode(mode); if (time != null) { - entry.setTime(time); + entry.setTime(DefaultTimeZoneOffset.INSTANCE.removeFrom(time)); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DefaultTimeZoneOffset.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DefaultTimeZoneOffset.java new file mode 100644 index 0000000000..449c339936 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DefaultTimeZoneOffset.java @@ -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.gradle.tasks.bundling; + +import java.nio.file.attribute.FileTime; +import java.util.TimeZone; +import java.util.zip.ZipEntry; + +/** + * Utility class that can be used change a UTC time based on the + * {@link java.util.TimeZone#getDefault() default TimeZone}. This is required because + * {@link ZipEntry#setTime(long)} expects times in the default timezone and not UTC. + * + * @author Phillip Webb + */ +class DefaultTimeZoneOffset { + + static final DefaultTimeZoneOffset INSTANCE = new DefaultTimeZoneOffset(TimeZone.getDefault()); + + private final TimeZone defaultTimeZone; + + DefaultTimeZoneOffset(TimeZone defaultTimeZone) { + this.defaultTimeZone = defaultTimeZone; + } + + /** + * Remove the default offset from the given time. + * @param time the time to remove the default offset from + * @return the time with the default offset removed + */ + FileTime removeFrom(FileTime time) { + return FileTime.fromMillis(removeFrom(time.toMillis())); + } + + /** + * Remove the default offset from the given time. + * @param time the time to remove the default offset from + * @return the time with the default offset removed + */ + long removeFrom(long time) { + return time - this.defaultTimeZone.getOffset(time); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java index 4fd6d854ff..4c17cc1cbf 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * 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. @@ -81,7 +81,7 @@ class LoaderZipEntries { private void prepareEntry(ZipArchiveEntry entry, int unixMode) { if (this.entryTime != null) { - entry.setTime(this.entryTime); + entry.setTime(DefaultTimeZoneOffset.INSTANCE.removeFrom(this.entryTime)); } entry.setUnixMode(unixMode); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java index d9a21ee3d5..532a65d245 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java @@ -389,18 +389,19 @@ abstract class AbstractBootArchiveTests { this.task.setPreserveFileTimestamps(false); executeTask(); assertThat(this.task.getArchiveFile().get().getAsFile()).exists(); + long expectedTime = DefaultTimeZoneOffset.INSTANCE.removeFrom(BootZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES); try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { Enumeration entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); - assertThat(entry.getTime()).isEqualTo(BootZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES); + assertThat(entry.getTime()).isEqualTo(expectedTime); } } } @Test void constantTimestampMatchesGradleInternalTimestamp() { - assertThat(BootZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES) + assertThat(DefaultTimeZoneOffset.INSTANCE.removeFrom(BootZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES)) .isEqualTo(ZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DefaultTimeZoneOffsetTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DefaultTimeZoneOffsetTests.java new file mode 100644 index 0000000000..7bae541a1b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DefaultTimeZoneOffsetTests.java @@ -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.gradle.tasks.bundling; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Calendar; +import java.util.TimeZone; + +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultTimeZoneOffset} + * + * @author Phillip Webb + */ +class DefaultTimeZoneOffsetTests { + + // gh-21005 + + @Test + void removeFromWithLongInDifferentTimeZonesReturnsSameValue() { + long time = OffsetDateTime.of(2000, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC).toInstant().toEpochMilli(); + TimeZone timeZone1 = TimeZone.getTimeZone("GMT"); + TimeZone timeZone2 = TimeZone.getTimeZone("GMT+8"); + TimeZone timeZone3 = TimeZone.getTimeZone("GMT-8"); + long result1 = new DefaultTimeZoneOffset(timeZone1).removeFrom(time); + long result2 = new DefaultTimeZoneOffset(timeZone2).removeFrom(time); + long result3 = new DefaultTimeZoneOffset(timeZone3).removeFrom(time); + long dosTime1 = toDosTime(Calendar.getInstance(timeZone1), result1); + long dosTime2 = toDosTime(Calendar.getInstance(timeZone2), result2); + long dosTime3 = toDosTime(Calendar.getInstance(timeZone3), result3); + assertThat(dosTime1).isEqualTo(dosTime2).isEqualTo(dosTime3); + } + + @Test + void removeFromWithFileTimeReturnsFileTime() { + long time = OffsetDateTime.of(2000, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC).toInstant().toEpochMilli(); + long result = new DefaultTimeZoneOffset(TimeZone.getTimeZone("GMT+8")).removeFrom(time); + assertThat(result).isNotEqualTo(time).isEqualTo(946656000000L); + } + + /** + * Identical functionality to package-private + * org.apache.commons.compress.archivers.zip.ZipUtil.toDosTime(Calendar, long, byte[], + * int) method used by {@link ZipArchiveOutputStream} to convert times. + * @param calendar the source calendar + * @param time the time to convert + * @return the DOS time + */ + private long toDosTime(Calendar calendar, long time) { + calendar.setTimeInMillis(time); + final int year = calendar.get(Calendar.YEAR); + final int month = calendar.get(Calendar.MONTH) + 1; + return ((year - 1980) << 25) | (month << 21) | (calendar.get(Calendar.DAY_OF_MONTH) << 16) + | (calendar.get(Calendar.HOUR_OF_DAY) << 11) | (calendar.get(Calendar.MINUTE) << 5) + | (calendar.get(Calendar.SECOND) >> 1); + } + +}