From 456327260bb17580e2d854b117c03f3682086007 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 4 Apr 2017 11:19:15 +0100 Subject: [PATCH] Fail fast when a Zip64 jar is encountered Previously, jars (either top-level or nested) in Zip64 format were treated as normal jar files. This would lead to a failure later on when an attempt was made to read an entry from the file. This commit updates the loader to fail fast when it encounters a Zip64 jar file. Such files are identified by the number of entries in the central directory end record being 0xFFFF. Closes gh-8735 --- .../boot/loader/archive/JarFileArchive.java | 12 +++- .../loader/jar/CentralDirectoryEndRecord.java | 9 ++- .../loader/archive/JarFileArchiveTests.java | 57 ++++++++++++++++++- 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java index a136897e12..a304503843 100755 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 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. @@ -100,8 +100,14 @@ public class JarFileArchive implements Archive { if (jarEntry.getComment().startsWith(UNPACK_MARKER)) { return getUnpackedNestedArchive(jarEntry); } - JarFile jarFile = this.jarFile.getNestedJarFile(jarEntry); - return new JarFileArchive(jarFile); + try { + JarFile jarFile = this.jarFile.getNestedJarFile(jarEntry); + return new JarFileArchive(jarFile); + } + catch (Exception ex) { + throw new IllegalStateException( + "Failed to get nested archive for entry " + entry.getName(), ex); + } } private Archive getUnpackedNestedArchive(JarEntry jarEntry) throws IOException { diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java index 04fd941088..d30140d579 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2017 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. @@ -24,6 +24,7 @@ import org.springframework.boot.loader.data.RandomAccessData; * A ZIP File "End of central directory record" (EOCD). * * @author Phillip Webb + * @author Andy Wilkinson * @see Zip File Format */ class CentralDirectoryEndRecord { @@ -118,7 +119,11 @@ class CentralDirectoryEndRecord { * @return the number of records in the zip */ public int getNumberOfRecords() { - return (int) Bytes.littleEndianValue(this.block, this.offset + 10, 2); + long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10, 2); + if (numberOfRecords == 0xFFFF) { + throw new IllegalStateException("Zip64 archives are not supported"); + } + return (int) numberOfRecords; } } diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java index ac7756d6d8..7474965874 100755 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 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. @@ -16,20 +16,30 @@ package org.springframework.boot.loader.archive; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; import java.net.URL; import java.util.HashMap; import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import org.springframework.boot.loader.TestJarCreator; import org.springframework.boot.loader.archive.Archive.Entry; +import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; /** * Tests for {@link JarFileArchive}. @@ -42,6 +52,9 @@ public class JarFileArchiveTests { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule + public ExpectedException thrown = ExpectedException.none(); + private File rootJarFile; private JarFileArchive archive; @@ -120,6 +133,48 @@ public class JarFileArchiveTests { assertThat(nested.getParent()).isEqualTo(anotherNested.getParent()); } + @Test + public void zip64ArchivesAreHandledGracefully() throws IOException { + File file = this.temporaryFolder.newFile("test.jar"); + FileCopyUtils.copy(writeZip64Jar(), file); + this.thrown.expectMessage(equalTo("Zip64 archives are not supported")); + new JarFileArchive(file); + } + + @Test + public void nestedZip64ArchivesAreHandledGracefully() throws IOException { + File file = this.temporaryFolder.newFile("test.jar"); + JarOutputStream output = new JarOutputStream(new FileOutputStream(file)); + JarEntry zip64JarEntry = new JarEntry("nested/zip64.jar"); + output.putNextEntry(zip64JarEntry); + byte[] zip64JarData = writeZip64Jar(); + zip64JarEntry.setSize(zip64JarData.length); + zip64JarEntry.setCompressedSize(zip64JarData.length); + zip64JarEntry.setMethod(ZipEntry.STORED); + CRC32 crc32 = new CRC32(); + crc32.update(zip64JarData); + zip64JarEntry.setCrc(crc32.getValue()); + output.write(zip64JarData); + output.closeEntry(); + output.close(); + JarFileArchive jarFileArchive = new JarFileArchive(file); + this.thrown.expectMessage( + equalTo("Failed to get nested archive for entry nested/zip64.jar")); + jarFileArchive + .getNestedArchive(getEntriesMap(jarFileArchive).get("nested/zip64.jar")); + } + + private byte[] writeZip64Jar() 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.closeEntry(); + } + jarOutput.close(); + return bytes.toByteArray(); + } + private Map getEntriesMap(Archive archive) { Map entries = new HashMap(); for (Archive.Entry entry : archive) {