Skip to content

Commit 69d34c9

Browse files
committed
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 998d59b). 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
1 parent 998d59b commit 69d34c9

File tree

5 files changed

+146
-9
lines changed

5 files changed

+146
-9
lines changed

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
import java.io.InputStream;
2323
import java.io.OutputStream;
2424
import java.io.OutputStreamWriter;
25-
import java.util.Calendar;
25+
import java.time.OffsetDateTime;
26+
import java.time.ZoneOffset;
2627
import java.util.Collection;
27-
import java.util.GregorianCalendar;
2828
import java.util.LinkedHashSet;
2929
import java.util.List;
3030
import java.util.Map;
@@ -67,8 +67,9 @@
6767
*/
6868
class BootZipCopyAction implements CopyAction {
6969

70-
static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = new GregorianCalendar(1980, Calendar.FEBRUARY, 1, 0, 0, 0)
71-
.getTimeInMillis();
70+
static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = OffsetDateTime.of(1980, 2, 1, 0, 0, 0, 0, ZoneOffset.UTC)
71+
.toInstant()
72+
.toEpochMilli();
7273

7374
private final File output;
7475

@@ -364,7 +365,7 @@ private void prepareEntry(ZipArchiveEntry entry, String name, Long time, int mod
364365
writeParentDirectoriesIfNecessary(name, time);
365366
entry.setUnixMode(mode);
366367
if (time != null) {
367-
entry.setTime(time);
368+
entry.setTime(DefaultTimeZoneOffset.INSTANCE.removeFrom(time));
368369
}
369370
}
370371

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.gradle.tasks.bundling;
18+
19+
import java.nio.file.attribute.FileTime;
20+
import java.util.TimeZone;
21+
import java.util.zip.ZipEntry;
22+
23+
/**
24+
* Utility class that can be used change a UTC time based on the
25+
* {@link java.util.TimeZone#getDefault() default TimeZone}. This is required because
26+
* {@link ZipEntry#setTime(long)} expects times in the default timezone and not UTC.
27+
*
28+
* @author Phillip Webb
29+
*/
30+
class DefaultTimeZoneOffset {
31+
32+
static final DefaultTimeZoneOffset INSTANCE = new DefaultTimeZoneOffset(TimeZone.getDefault());
33+
34+
private final TimeZone defaultTimeZone;
35+
36+
DefaultTimeZoneOffset(TimeZone defaultTimeZone) {
37+
this.defaultTimeZone = defaultTimeZone;
38+
}
39+
40+
/**
41+
* Remove the default offset from the given time.
42+
* @param time the time to remove the default offset from
43+
* @return the time with the default offset removed
44+
*/
45+
FileTime removeFrom(FileTime time) {
46+
return FileTime.fromMillis(removeFrom(time.toMillis()));
47+
}
48+
49+
/**
50+
* Remove the default offset from the given time.
51+
* @param time the time to remove the default offset from
52+
* @return the time with the default offset removed
53+
*/
54+
long removeFrom(long time) {
55+
return time - this.defaultTimeZone.getOffset(time);
56+
}
57+
58+
}

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -81,7 +81,7 @@ private void writeClass(ZipArchiveEntry entry, ZipInputStream in, ZipArchiveOutp
8181

8282
private void prepareEntry(ZipArchiveEntry entry, int unixMode) {
8383
if (this.entryTime != null) {
84-
entry.setTime(this.entryTime);
84+
entry.setTime(DefaultTimeZoneOffset.INSTANCE.removeFrom(this.entryTime));
8585
}
8686
entry.setUnixMode(unixMode);
8787
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,18 +351,19 @@ void fileTimestampPreservationCanBeDisabled() throws IOException {
351351
this.task.setPreserveFileTimestamps(false);
352352
executeTask();
353353
assertThat(this.task.getArchiveFile().get().getAsFile()).exists();
354+
long expectedTime = DefaultTimeZoneOffset.INSTANCE.removeFrom(BootZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES);
354355
try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) {
355356
Enumeration<JarEntry> entries = jarFile.entries();
356357
while (entries.hasMoreElements()) {
357358
JarEntry entry = entries.nextElement();
358-
assertThat(entry.getTime()).isEqualTo(BootZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES);
359+
assertThat(entry.getTime()).isEqualTo(expectedTime);
359360
}
360361
}
361362
}
362363

363364
@Test
364365
void constantTimestampMatchesGradleInternalTimestamp() {
365-
assertThat(BootZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES)
366+
assertThat(DefaultTimeZoneOffset.INSTANCE.removeFrom(BootZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES))
366367
.isEqualTo(ZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES);
367368
}
368369

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.gradle.tasks.bundling;
18+
19+
import java.time.OffsetDateTime;
20+
import java.time.ZoneOffset;
21+
import java.util.Calendar;
22+
import java.util.TimeZone;
23+
24+
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
25+
import org.junit.jupiter.api.Test;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
29+
/**
30+
* Tests for {@link DefaultTimeZoneOffset}
31+
*
32+
* @author Phillip Webb
33+
*/
34+
class DefaultTimeZoneOffsetTests {
35+
36+
// gh-21005
37+
38+
@Test
39+
void removeFromWithLongInDifferentTimeZonesReturnsSameValue() {
40+
long time = OffsetDateTime.of(2000, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC).toInstant().toEpochMilli();
41+
TimeZone timeZone1 = TimeZone.getTimeZone("GMT");
42+
TimeZone timeZone2 = TimeZone.getTimeZone("GMT+8");
43+
TimeZone timeZone3 = TimeZone.getTimeZone("GMT-8");
44+
long result1 = new DefaultTimeZoneOffset(timeZone1).removeFrom(time);
45+
long result2 = new DefaultTimeZoneOffset(timeZone2).removeFrom(time);
46+
long result3 = new DefaultTimeZoneOffset(timeZone3).removeFrom(time);
47+
long dosTime1 = toDosTime(Calendar.getInstance(timeZone1), result1);
48+
long dosTime2 = toDosTime(Calendar.getInstance(timeZone2), result2);
49+
long dosTime3 = toDosTime(Calendar.getInstance(timeZone3), result3);
50+
assertThat(dosTime1).isEqualTo(dosTime2).isEqualTo(dosTime3);
51+
}
52+
53+
@Test
54+
void removeFromWithFileTimeReturnsFileTime() {
55+
long time = OffsetDateTime.of(2000, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC).toInstant().toEpochMilli();
56+
long result = new DefaultTimeZoneOffset(TimeZone.getTimeZone("GMT+8")).removeFrom(time);
57+
assertThat(result).isNotEqualTo(time).isEqualTo(946656000000L);
58+
}
59+
60+
/**
61+
* Identical functionality to package-private
62+
* org.apache.commons.compress.archivers.zip.ZipUtil.toDosTime(Calendar, long, byte[],
63+
* int) method used by {@link ZipArchiveOutputStream} to convert times.
64+
* @param calendar the source calendar
65+
* @param time the time to convert
66+
* @return the DOS time
67+
*/
68+
private long toDosTime(Calendar calendar, long time) {
69+
calendar.setTimeInMillis(time);
70+
final int year = calendar.get(Calendar.YEAR);
71+
final int month = calendar.get(Calendar.MONTH) + 1;
72+
return ((year - 1980) << 25) | (month << 21) | (calendar.get(Calendar.DAY_OF_MONTH) << 16)
73+
| (calendar.get(Calendar.HOUR_OF_DAY) << 11) | (calendar.get(Calendar.MINUTE) << 5)
74+
| (calendar.get(Calendar.SECOND) >> 1);
75+
}
76+
77+
}

0 commit comments

Comments
 (0)