Browse Source

Apply consistent timestamps to files added to a fat archive

Update logic in `BootZipCopyAction` to align with the recent changes
made in the Maven plugin (commit 998d59b7). Timestamps are now
specified in UTC and offset against the default timezone before being
written.

Removing the offset from our UTC time before calling `entry.setTime()`
ensures that we get consistent bytes in the zip file when the output
stream reapplies the offset during write.

Closes gh-21005
pull/34587/head
Phillip Webb 3 years ago
parent
commit
69d34c96bf
  1. 11
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java
  2. 58
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DefaultTimeZoneOffset.java
  3. 4
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java
  4. 5
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java
  5. 77
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DefaultTimeZoneOffsetTests.java

11
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; @@ -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.LinkedHashSet;
import java.util.List;
import java.util.Map;
@ -67,8 +67,9 @@ import org.springframework.util.StringUtils; @@ -67,8 +67,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 final File output;
@ -364,7 +365,7 @@ class BootZipCopyAction implements CopyAction { @@ -364,7 +365,7 @@ class BootZipCopyAction implements CopyAction {
writeParentDirectoriesIfNecessary(name, time);
entry.setUnixMode(mode);
if (time != null) {
entry.setTime(time);
entry.setTime(DefaultTimeZoneOffset.INSTANCE.removeFrom(time));
}
}

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

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

5
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java

@ -351,18 +351,19 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> { @@ -351,18 +351,19 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
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<JarEntry> 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);
}

77
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 @@ @@ -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);
}
}
Loading…
Cancel
Save