diff --git a/.gitignore b/.gitignore index 0b836fe4ea..d01ecf75ca 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ .settings .springBeans /build +.vscode /code MANIFEST.MF _site/ @@ -38,4 +39,4 @@ transaction-logs secrets.yml .gradletasknamecache .sts4-cache -.mvn/.gradle-enterprise/gradle-enterprise-workspace-id \ No newline at end of file +.mvn/.gradle-enterprise/gradle-enterprise-workspace-id diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java index aed07d1bc7..192b3a8788 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -32,20 +32,21 @@ import java.util.jar.Manifest; */ class JarEntry extends java.util.jar.JarEntry implements FileHeader { + private final int index; + private final AsciiBytes name; private final AsciiBytes headerName; - private Certificate[] certificates; - - private CodeSigner[] codeSigners; - private final JarFile jarFile; private long localHeaderOffset; - JarEntry(JarFile jarFile, CentralDirectoryFileHeader header, AsciiBytes nameAlias) { + private volatile JarEntryCertification certification; + + JarEntry(JarFile jarFile, int index, CentralDirectoryFileHeader header, AsciiBytes nameAlias) { super((nameAlias != null) ? nameAlias.toString() : header.getName().toString()); + this.index = index; this.name = (nameAlias != null) ? nameAlias : header.getName(); this.headerName = header.getName(); this.jarFile = jarFile; @@ -59,6 +60,10 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader { setExtra(header.getExtra()); } + int getIndex() { + return this.index; + } + AsciiBytes getAsciiBytesName() { return this.name; } @@ -85,23 +90,24 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader { @Override public Certificate[] getCertificates() { - if (this.jarFile.isSigned() && this.certificates == null) { - this.jarFile.setupEntryCertificates(this); - } - return this.certificates; + return getCertification().getCertificates(); } @Override public CodeSigner[] getCodeSigners() { - if (this.jarFile.isSigned() && this.codeSigners == null) { - this.jarFile.setupEntryCertificates(this); - } - return this.codeSigners; + return getCertification().getCodeSigners(); } - void setCertificates(java.util.jar.JarEntry entry) { - this.certificates = entry.getCertificates(); - this.codeSigners = entry.getCodeSigners(); + private JarEntryCertification getCertification() { + if (!this.jarFile.isSigned()) { + return JarEntryCertification.NONE; + } + JarEntryCertification certification = this.certification; + if (certification == null) { + certification = this.jarFile.getCertification(this); + this.certification = certification; + } + return certification; } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java new file mode 100644 index 0000000000..cbf66412e2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2020 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.loader.jar; + +import java.security.CodeSigner; +import java.security.cert.Certificate; + +/** + * {@link Certificate} and {@link CodeSigner} details for a {@link JarEntry} from a signed + * {@link JarFile}. + * + * @author Phillip Webb + */ +class JarEntryCertification { + + static final JarEntryCertification NONE = new JarEntryCertification(null, null); + + private final Certificate[] certificates; + + private final CodeSigner[] codeSigners; + + JarEntryCertification(Certificate[] certificates, CodeSigner[] codeSigners) { + this.certificates = certificates; + this.codeSigners = codeSigners; + } + + Certificate[] getCertificates() { + return (this.certificates != null) ? this.certificates.clone() : null; + } + + CodeSigner[] getCodeSigners() { + return (this.codeSigners != null) ? this.codeSigners.clone() : null; + } + + static JarEntryCertification from(java.util.jar.JarEntry certifiedEntry) { + Certificate[] certificates = (certifiedEntry != null) ? certifiedEntry.getCertificates() : null; + CodeSigner[] codeSigners = (certifiedEntry != null) ? certifiedEntry.getCodeSigners() : null; + if (certificates == null && codeSigners == null) { + return NONE; + } + return new JarEntryCertification(certificates, codeSigners); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java index 2649588f6e..eb4768543e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java @@ -27,7 +27,6 @@ import java.net.URLStreamHandlerFactory; import java.util.Enumeration; import java.util.Iterator; import java.util.function.Supplier; -import java.util.jar.JarInputStream; import java.util.jar.Manifest; import java.util.zip.ZipEntry; @@ -387,33 +386,15 @@ public class JarFile extends java.util.jar.JarFile { return this.signed; } - void setupEntryCertificates(JarEntry entry) { - // Fallback to JarInputStream to obtain certificates, not fast but hopefully not - // happening that often. + JarEntryCertification getCertification(JarEntry entry) { try { - try (JarInputStream inputStream = new JarInputStream(getData().getInputStream())) { - java.util.jar.JarEntry certEntry = inputStream.getNextJarEntry(); - while (certEntry != null) { - inputStream.closeEntry(); - if (entry.getName().equals(certEntry.getName())) { - setCertificates(entry, certEntry); - } - setCertificates(getJarEntry(certEntry.getName()), certEntry); - certEntry = inputStream.getNextJarEntry(); - } - } + return this.entries.getCertification(entry); } catch (IOException ex) { throw new IllegalStateException(ex); } } - private void setCertificates(JarEntry entry, java.util.jar.JarEntry certEntry) { - if (entry != null) { - entry.setCertificates(certEntry); - } - } - public void clearCache() { this.entries.clearCache(); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java index 1c983663fd..5645abdd36 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -26,6 +26,7 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.jar.Attributes; import java.util.jar.Attributes.Name; +import java.util.jar.JarInputStream; import java.util.jar.Manifest; import java.util.zip.ZipEntry; @@ -91,14 +92,13 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable { private Boolean multiReleaseJar; + private JarEntryCertification[] certifications; + private final Map entriesCache = Collections .synchronizedMap(new LinkedHashMap(16, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry eldest) { - if (JarFileEntries.this.jarFile.isSigned()) { - return false; - } return size() >= ENTRY_CACHE_SIZE; } @@ -313,7 +313,7 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable { FileHeader entry = (cached != null) ? cached : CentralDirectoryFileHeader .fromRandomAccessData(this.centralDirectoryData, this.centralDirectoryOffsets[index], this.filter); if (CentralDirectoryFileHeader.class.equals(entry.getClass()) && type.equals(JarEntry.class)) { - entry = new JarEntry(this.jarFile, (CentralDirectoryFileHeader) entry, nameAlias); + entry = new JarEntry(this.jarFile, index, (CentralDirectoryFileHeader) entry, nameAlias); } if (cacheEntry && cached != entry) { this.entriesCache.put(index, entry); @@ -344,6 +344,41 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable { return (this.filter != null) ? this.filter.apply(name) : name; } + JarEntryCertification getCertification(JarEntry entry) throws IOException { + JarEntryCertification[] certifications = this.certifications; + if (certifications == null) { + certifications = new JarEntryCertification[this.size]; + // We fallback to use JarInputStream to obtain the certs. This isn't that + // fast, but hopefully doesn't happen too often. + try (JarInputStream certifiedJarStream = new JarInputStream(this.jarFile.getData().getInputStream())) { + java.util.jar.JarEntry certifiedEntry = null; + while ((certifiedEntry = certifiedJarStream.getNextJarEntry()) != null) { + int index = getEntryIndex(certifiedEntry.getName()); + if (index != -1) { + certifications[index] = JarEntryCertification.from(certifiedEntry); + } + certifiedJarStream.closeEntry(); + } + } + this.certifications = certifications; + } + JarEntryCertification certification = certifications[entry.getIndex()]; + return (certification != null) ? certification : JarEntryCertification.NONE; + } + + private int getEntryIndex(CharSequence name) { + int hashCode = AsciiBytes.hashCode(name); + int index = getFirstIndex(hashCode); + while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) { + CentralDirectoryFileHeader candidate = getEntry(index, CentralDirectoryFileHeader.class, false, null); + if (candidate.hasName(name, NO_SUFFIX)) { + return index; + } + index++; + } + return -1; + } + /** * Iterator for contained entries. */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java index 3459f76c18..50daa7a0c4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java @@ -44,6 +44,7 @@ import org.springframework.boot.loader.TestJarCreator; import org.springframework.boot.loader.data.RandomAccessDataFile; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.FileCopyUtils; +import org.springframework.util.StopWatch; import org.springframework.util.StreamUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -375,29 +376,33 @@ public class JarFileTests { @Test public void verifySignedJar() throws Exception { - String classpath = System.getProperty("java.class.path"); - String[] entries = classpath.split(System.getProperty("path.separator")); - String signedJarFile = null; - for (String entry : entries) { - if (entry.contains("bcprov")) { - signedJarFile = entry; + File signedJarFile = getSignedJarFile(); + assertThat(signedJarFile).exists(); + try (java.util.jar.JarFile expected = new java.util.jar.JarFile(signedJarFile)) { + try (JarFile actual = new JarFile(signedJarFile)) { + StopWatch stopWatch = new StopWatch(); + Enumeration actualEntries = actual.entries(); + while (actualEntries.hasMoreElements()) { + JarEntry actualEntry = actualEntries.nextElement(); + java.util.jar.JarEntry expectedEntry = expected.getJarEntry(actualEntry.getName()); + assertThat(actualEntry.getCertificates()).as(actualEntry.getName()) + .isEqualTo(expectedEntry.getCertificates()); + assertThat(actualEntry.getCodeSigners()).as(actualEntry.getName()) + .isEqualTo(expectedEntry.getCodeSigners()); + } + assertThat(stopWatch.getTotalTimeSeconds()).isLessThan(3.0); } } - assertThat(signedJarFile).isNotNull(); - java.util.jar.JarFile jarFile = new JarFile(new File(signedJarFile)); - jarFile.getManifest(); - Enumeration jarEntries = jarFile.entries(); - while (jarEntries.hasMoreElements()) { - JarEntry jarEntry = jarEntries.nextElement(); - InputStream inputStream = jarFile.getInputStream(jarEntry); - inputStream.skip(Long.MAX_VALUE); - inputStream.close(); - if (!jarEntry.getName().startsWith("META-INF") && !jarEntry.isDirectory() - && !jarEntry.getName().endsWith("TigerDigest.class")) { - assertThat(jarEntry.getCertificates()).isNotNull(); + } + + private File getSignedJarFile() { + String[] entries = System.getProperty("java.class.path").split(System.getProperty("path.separator")); + for (String entry : entries) { + if (entry.contains("bcprov")) { + return new File(entry); } } - jarFile.close(); + return null; } @Test