Commit 02ac0897 authored by Andy Wilkinson's avatar Andy Wilkinson

Polish "Support zip64 jars"

See gh-16091
parent 1917e1ea
...@@ -404,6 +404,10 @@ Currently, some tools do not accept this format, so you may not always be able t ...@@ -404,6 +404,10 @@ Currently, some tools do not accept this format, so you may not always be able t
For example, `jar -xf` may silently fail to extract a jar or war that has been made fully executable. For example, `jar -xf` may silently fail to extract a jar or war that has been made fully executable.
It is recommended that you make your jar or war fully executable only if you intend to execute it directly, rather than running it with `java -jar`or deploying it to a servlet container. It is recommended that you make your jar or war fully executable only if you intend to execute it directly, rather than running it with `java -jar`or deploying it to a servlet container.
CAUTION: A zip64-format jar file cannot be made fully executable.
Attempting to do so will result in a jar file that is reported as corrupt when executed directly or with `java -jar`.
A standard-format jar file that contains one or more zip64-format nested jars can be fully executable.
To create a '`fully executable`' jar with Maven, use the following plugin configuration: To create a '`fully executable`' jar with Maven, use the following plugin configuration:
[source,xml,indent=0,subs="verbatim,quotes,attributes"] [source,xml,indent=0,subs="verbatim,quotes,attributes"]
......
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
package org.springframework.boot.loader.jar; package org.springframework.boot.loader.jar;
import java.io.IOException; import java.io.IOException;
import java.util.Optional;
import org.springframework.boot.loader.data.RandomAccessData; import org.springframework.boot.loader.data.RandomAccessData;
...@@ -45,7 +44,7 @@ class CentralDirectoryEndRecord { ...@@ -45,7 +44,7 @@ class CentralDirectoryEndRecord {
private static final int READ_BLOCK_SIZE = 256; private static final int READ_BLOCK_SIZE = 256;
private final Optional<Zip64End> zip64End; private final Zip64End zip64End;
private byte[] block; private byte[] block;
...@@ -76,8 +75,7 @@ class CentralDirectoryEndRecord { ...@@ -76,8 +75,7 @@ class CentralDirectoryEndRecord {
this.offset = this.block.length - this.size; this.offset = this.block.length - this.size;
} }
int startOfCentralDirectoryEndRecord = (int) (data.getSize() - this.size); int startOfCentralDirectoryEndRecord = (int) (data.getSize() - this.size);
this.zip64End = Optional.ofNullable( this.zip64End = isZip64() ? new Zip64End(data, startOfCentralDirectoryEndRecord) : null;
isZip64() ? new Zip64End(data, startOfCentralDirectoryEndRecord) : null);
} }
private byte[] createBlockFromEndOfData(RandomAccessData data, int size) throws IOException { private byte[] createBlockFromEndOfData(RandomAccessData data, int size) throws IOException {
...@@ -94,6 +92,10 @@ class CentralDirectoryEndRecord { ...@@ -94,6 +92,10 @@ class CentralDirectoryEndRecord {
return this.size == MINIMUM_SIZE + commentLength; return this.size == MINIMUM_SIZE + commentLength;
} }
private boolean isZip64() {
return (int) Bytes.littleEndianValue(this.block, this.offset + 10, 2) == ZIP64_MAGICCOUNT;
}
/** /**
* Returns the location in the data that the archive actually starts. For most files * Returns the location in the data that the archive actually starts. For most files
* the archive data will start at 0, however, it is possible to have prefixed bytes * the archive data will start at 0, however, it is possible to have prefixed bytes
...@@ -104,10 +106,9 @@ class CentralDirectoryEndRecord { ...@@ -104,10 +106,9 @@ class CentralDirectoryEndRecord {
long getStartOfArchive(RandomAccessData data) { long getStartOfArchive(RandomAccessData data) {
long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4); long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4);
long specifiedOffset = Bytes.littleEndianValue(this.block, this.offset + 16, 4); long specifiedOffset = Bytes.littleEndianValue(this.block, this.offset + 16, 4);
long zip64EndSize = this.zip64End.map((x) -> x.getSize()).orElse(0L); long zip64EndSize = (this.zip64End != null) ? this.zip64End.getSize() : 0L;
int zip64LocSize = this.zip64End.map((x) -> Zip64Locator.ZIP64_LOCSIZE).orElse(0); int zip64LocSize = (this.zip64End != null) ? Zip64Locator.ZIP64_LOCSIZE : 0;
long actualOffset = data.getSize() - this.size - length - zip64EndSize long actualOffset = data.getSize() - this.size - length - zip64EndSize - zip64LocSize;
- zip64LocSize;
return actualOffset - specifiedOffset; return actualOffset - specifiedOffset;
} }
...@@ -118,14 +119,12 @@ class CentralDirectoryEndRecord { ...@@ -118,14 +119,12 @@ class CentralDirectoryEndRecord {
* @return the central directory data * @return the central directory data
*/ */
RandomAccessData getCentralDirectory(RandomAccessData data) { RandomAccessData getCentralDirectory(RandomAccessData data) {
if (isZip64()) { if (this.zip64End != null) {
return this.zip64End.get().getCentratDirectory(data); return this.zip64End.getCentralDirectory(data);
}
else {
long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4);
long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4);
return data.getSubsection(offset, length);
} }
long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4);
long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4);
return data.getSubsection(offset, length);
} }
/** /**
...@@ -133,19 +132,17 @@ class CentralDirectoryEndRecord { ...@@ -133,19 +132,17 @@ class CentralDirectoryEndRecord {
* @return the number of records in the zip * @return the number of records in the zip
*/ */
int getNumberOfRecords() { int getNumberOfRecords() {
if (isZip64()) { if (this.zip64End != null) {
return this.zip64End.get().getNumberOfRecords(); return this.zip64End.getNumberOfRecords();
}
else {
long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10,
2);
return (int) numberOfRecords;
} }
long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10, 2);
return (int) numberOfRecords;
} }
boolean isZip64() { String getComment() {
return (int) Bytes.littleEndianValue(this.block, this.offset + 10, int commentLength = (int) Bytes.littleEndianValue(this.block, this.offset + COMMENT_LENGTH_OFFSET, 2);
2) == ZIP64_MAGICCOUNT; AsciiBytes comment = new AsciiBytes(this.block, this.offset + COMMENT_LENGTH_OFFSET + 2, commentLength);
return comment.toString();
} }
/** /**
...@@ -154,11 +151,13 @@ class CentralDirectoryEndRecord { ...@@ -154,11 +151,13 @@ class CentralDirectoryEndRecord {
* @see <a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT">Chapter * @see <a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT">Chapter
* 4.3.14 of Zip64 specification</a> * 4.3.14 of Zip64 specification</a>
*/ */
private static class Zip64End { private static final class Zip64End {
static final int ZIP64_ENDTOT = 32; // total number of entries private static final int ZIP64_ENDTOT = 32; // total number of entries
static final int ZIP64_ENDSIZ = 40; // central directory size in bytes
static final int ZIP64_ENDOFF = 48; // offset of first CEN header private static final int ZIP64_ENDSIZ = 40; // central directory size in bytes
private static final int ZIP64_ENDOFF = 48; // offset of first CEN header
private final Zip64Locator locator; private final Zip64Locator locator;
...@@ -168,12 +167,11 @@ class CentralDirectoryEndRecord { ...@@ -168,12 +167,11 @@ class CentralDirectoryEndRecord {
private int numberOfRecords; private int numberOfRecords;
Zip64End(RandomAccessData data, int centratDirectoryEndOffset) private Zip64End(RandomAccessData data, int centratDirectoryEndOffset) throws IOException {
throws IOException {
this(data, new Zip64Locator(data, centratDirectoryEndOffset)); this(data, new Zip64Locator(data, centratDirectoryEndOffset));
} }
Zip64End(RandomAccessData data, Zip64Locator locator) throws IOException { private Zip64End(RandomAccessData data, Zip64Locator locator) throws IOException {
this.locator = locator; this.locator = locator;
byte[] block = data.read(locator.getZip64EndOffset(), 56); byte[] block = data.read(locator.getZip64EndOffset(), 56);
this.centralDirectoryOffset = Bytes.littleEndianValue(block, ZIP64_ENDOFF, 8); this.centralDirectoryOffset = Bytes.littleEndianValue(block, ZIP64_ENDOFF, 8);
...@@ -185,7 +183,7 @@ class CentralDirectoryEndRecord { ...@@ -185,7 +183,7 @@ class CentralDirectoryEndRecord {
* Return the size of this zip 64 end of central directory record. * Return the size of this zip 64 end of central directory record.
* @return size of this zip 64 end of central directory record * @return size of this zip 64 end of central directory record
*/ */
public long getSize() { private long getSize() {
return this.locator.getZip64EndSize(); return this.locator.getZip64EndSize();
} }
...@@ -195,16 +193,15 @@ class CentralDirectoryEndRecord { ...@@ -195,16 +193,15 @@ class CentralDirectoryEndRecord {
* @param data the source data * @param data the source data
* @return the central directory data * @return the central directory data
*/ */
public RandomAccessData getCentratDirectory(RandomAccessData data) { private RandomAccessData getCentralDirectory(RandomAccessData data) {
return data.getSubsection(this.centralDirectoryOffset, return data.getSubsection(this.centralDirectoryOffset, this.centralDirectoryLength);
this.centralDirectoryLength);
} }
/** /**
* Return the number of entries in the zip64 archive. * Return the number of entries in the zip64 archive.
* @return the number of records in the zip * @return the number of records in the zip
*/ */
public int getNumberOfRecords() { private int getNumberOfRecords() {
return this.numberOfRecords; return this.numberOfRecords;
} }
...@@ -216,7 +213,7 @@ class CentralDirectoryEndRecord { ...@@ -216,7 +213,7 @@ class CentralDirectoryEndRecord {
* @see <a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT">Chapter * @see <a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT">Chapter
* 4.3.15 of Zip64 specification</a> * 4.3.15 of Zip64 specification</a>
*/ */
private static class Zip64Locator { private static final class Zip64Locator {
static final int ZIP64_LOCSIZE = 20; // locator size static final int ZIP64_LOCSIZE = 20; // locator size
static final int ZIP64_LOCOFF = 8; // offset of zip64 end static final int ZIP64_LOCOFF = 8; // offset of zip64 end
...@@ -225,8 +222,7 @@ class CentralDirectoryEndRecord { ...@@ -225,8 +222,7 @@ class CentralDirectoryEndRecord {
private final int offset; private final int offset;
Zip64Locator(RandomAccessData data, int centralDirectoryEndOffset) private Zip64Locator(RandomAccessData data, int centralDirectoryEndOffset) throws IOException {
throws IOException {
this.offset = centralDirectoryEndOffset - ZIP64_LOCSIZE; this.offset = centralDirectoryEndOffset - ZIP64_LOCSIZE;
byte[] block = data.read(this.offset, ZIP64_LOCSIZE); byte[] block = data.read(this.offset, ZIP64_LOCSIZE);
this.zip64EndOffset = Bytes.littleEndianValue(block, ZIP64_LOCOFF, 8); this.zip64EndOffset = Bytes.littleEndianValue(block, ZIP64_LOCOFF, 8);
...@@ -236,7 +232,7 @@ class CentralDirectoryEndRecord { ...@@ -236,7 +232,7 @@ class CentralDirectoryEndRecord {
* Return the size of the zip 64 end record located by this zip64 end locator. * Return the size of the zip 64 end record located by this zip64 end locator.
* @return size of the zip 64 end record located by this zip64 end locator * @return size of the zip 64 end record located by this zip64 end locator
*/ */
public long getZip64EndSize() { private long getZip64EndSize() {
return this.offset - this.zip64EndOffset; return this.offset - this.zip64EndOffset;
} }
...@@ -244,16 +240,10 @@ class CentralDirectoryEndRecord { ...@@ -244,16 +240,10 @@ class CentralDirectoryEndRecord {
* Return the offset to locate {@link Zip64End}. * Return the offset to locate {@link Zip64End}.
* @return offset of the Zip64 end of central directory record * @return offset of the Zip64 end of central directory record
*/ */
public long getZip64EndOffset() { private long getZip64EndOffset() {
return this.zip64EndOffset; return this.zip64EndOffset;
} }
} }
String getComment() {
int commentLength = (int) Bytes.littleEndianValue(this.block, this.offset + COMMENT_LENGTH_OFFSET, 2);
AsciiBytes comment = new AsciiBytes(this.block, this.offset + COMMENT_LENGTH_OFFSET + 2, commentLength);
return comment.toString();
}
} }
...@@ -143,14 +143,15 @@ class JarFileArchiveTests { ...@@ -143,14 +143,15 @@ class JarFileArchiveTests {
} }
@Test @Test
void filesInzip64ArchivesAreAllListed() throws IOException { void filesInZip64ArchivesAreAllListed() throws IOException {
File file = new File(this.tempDir, "test.jar"); File file = new File(this.tempDir, "test.jar");
FileCopyUtils.copy(writeZip64Jar(), file); FileCopyUtils.copy(writeZip64Jar(), file);
JarFileArchive zip64Archive = new JarFileArchive(file); try (JarFileArchive zip64Archive = new JarFileArchive(file)) {
Iterator<Entry> it = zip64Archive.iterator(); Iterator<Entry> entries = zip64Archive.iterator();
for (int i = 0; i < 65537; i++) { for (int i = 0; i < 65537; i++) {
assertThat(it.hasNext()).as(i + "nth file is present").isTrue(); assertThat(entries.hasNext()).as(i + "nth file is present").isTrue();
it.next(); entries.next();
}
} }
} }
......
...@@ -16,19 +16,26 @@ ...@@ -16,19 +16,26 @@
package org.springframework.boot.loader.jar; package org.springframework.boot.loader.jar;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.FilePermission; import java.io.FilePermission;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.net.URLClassLoader; import java.net.URLClassLoader;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarInputStream; import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipFile; import java.util.zip.ZipFile;
...@@ -512,6 +519,65 @@ class JarFileTests { ...@@ -512,6 +519,65 @@ class JarFileTests {
} }
} }
@Test
void zip64JarCanBeRead() throws Exception {
File zip64Jar = new File(this.tempDir, "zip64.jar");
FileCopyUtils.copy(zip64Jar(), zip64Jar);
try (JarFile zip64JarFile = new JarFile(zip64Jar)) {
List<JarEntry> entries = Collections.list(zip64JarFile.entries());
assertThat(entries).hasSize(65537);
for (int i = 0; i < entries.size(); i++) {
JarEntry entry = entries.get(i);
InputStream entryInput = zip64JarFile.getInputStream(entry);
String contents = StreamUtils.copyToString(entryInput, StandardCharsets.UTF_8);
assertThat(contents).isEqualTo("Entry " + (i + 1));
}
}
}
@Test
void nestedZip64JarCanBeRead() throws Exception {
File outer = new File(this.tempDir, "outer.jar");
try (JarOutputStream jarOutput = new JarOutputStream(new FileOutputStream(outer))) {
JarEntry nestedEntry = new JarEntry("nested-zip64.jar");
byte[] contents = zip64Jar();
nestedEntry.setSize(contents.length);
nestedEntry.setCompressedSize(contents.length);
CRC32 crc32 = new CRC32();
crc32.update(contents);
nestedEntry.setCrc(crc32.getValue());
nestedEntry.setMethod(ZipEntry.STORED);
jarOutput.putNextEntry(nestedEntry);
jarOutput.write(contents);
jarOutput.closeEntry();
}
try (JarFile outerJarFile = new JarFile(outer)) {
try (JarFile nestedZip64JarFile = outerJarFile
.getNestedJarFile(outerJarFile.getJarEntry("nested-zip64.jar"))) {
List<JarEntry> entries = Collections.list(nestedZip64JarFile.entries());
assertThat(entries).hasSize(65537);
for (int i = 0; i < entries.size(); i++) {
JarEntry entry = entries.get(i);
InputStream entryInput = nestedZip64JarFile.getInputStream(entry);
String contents = StreamUtils.copyToString(entryInput, StandardCharsets.UTF_8);
assertThat(contents).isEqualTo("Entry " + (i + 1));
}
}
}
}
private byte[] zip64Jar() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
JarOutputStream jarOutput = new JarOutputStream(bytes);
for (int i = 0; i < 65537; i++) {
jarOutput.putNextEntry(new JarEntry(i + ".dat"));
jarOutput.write(("Entry " + (i + 1)).getBytes(StandardCharsets.UTF_8));
jarOutput.closeEntry();
}
jarOutput.close();
return bytes.toByteArray();
}
private int getJavaVersion() { private int getJavaVersion() {
try { try {
Object runtimeVersion = Runtime.class.getMethod("version").invoke(null); Object runtimeVersion = Runtime.class.getMethod("version").invoke(null);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment