Merge pull request #19041 from mathieufortin01

* pr/19041:
  Polish 'Fix signed jar performance issues'
  Fix signed jar performance issues
  Ignore Visual Studio Code Files

Closes gh-19041
2.1.x
Phillip Webb 4 years ago
commit 895ff9c72d

3
.gitignore vendored

@ -19,6 +19,7 @@
.settings .settings
.springBeans .springBeans
/build /build
.vscode
/code /code
MANIFEST.MF MANIFEST.MF
_site/ _site/
@ -38,4 +39,4 @@ transaction-logs
secrets.yml secrets.yml
.gradletasknamecache .gradletasknamecache
.sts4-cache .sts4-cache
.mvn/.gradle-enterprise/gradle-enterprise-workspace-id .mvn/.gradle-enterprise/gradle-enterprise-workspace-id

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 { class JarEntry extends java.util.jar.JarEntry implements FileHeader {
private final int index;
private final AsciiBytes name; private final AsciiBytes name;
private final AsciiBytes headerName; private final AsciiBytes headerName;
private Certificate[] certificates;
private CodeSigner[] codeSigners;
private final JarFile jarFile; private final JarFile jarFile;
private long localHeaderOffset; 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()); super((nameAlias != null) ? nameAlias.toString() : header.getName().toString());
this.index = index;
this.name = (nameAlias != null) ? nameAlias : header.getName(); this.name = (nameAlias != null) ? nameAlias : header.getName();
this.headerName = header.getName(); this.headerName = header.getName();
this.jarFile = jarFile; this.jarFile = jarFile;
@ -59,6 +60,10 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader {
setExtra(header.getExtra()); setExtra(header.getExtra());
} }
int getIndex() {
return this.index;
}
AsciiBytes getAsciiBytesName() { AsciiBytes getAsciiBytesName() {
return this.name; return this.name;
} }
@ -85,23 +90,24 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader {
@Override @Override
public Certificate[] getCertificates() { public Certificate[] getCertificates() {
if (this.jarFile.isSigned() && this.certificates == null) { return getCertification().getCertificates();
this.jarFile.setupEntryCertificates(this);
}
return this.certificates;
} }
@Override @Override
public CodeSigner[] getCodeSigners() { public CodeSigner[] getCodeSigners() {
if (this.jarFile.isSigned() && this.codeSigners == null) { return getCertification().getCodeSigners();
this.jarFile.setupEntryCertificates(this);
}
return this.codeSigners;
} }
void setCertificates(java.util.jar.JarEntry entry) { private JarEntryCertification getCertification() {
this.certificates = entry.getCertificates(); if (!this.jarFile.isSigned()) {
this.codeSigners = entry.getCodeSigners(); return JarEntryCertification.NONE;
}
JarEntryCertification certification = this.certification;
if (certification == null) {
certification = this.jarFile.getCertification(this);
this.certification = certification;
}
return certification;
} }
@Override @Override

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

@ -27,7 +27,6 @@ import java.net.URLStreamHandlerFactory;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.Iterator; import java.util.Iterator;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
@ -387,33 +386,15 @@ public class JarFile extends java.util.jar.JarFile {
return this.signed; return this.signed;
} }
void setupEntryCertificates(JarEntry entry) { JarEntryCertification getCertification(JarEntry entry) {
// Fallback to JarInputStream to obtain certificates, not fast but hopefully not
// happening that often.
try { try {
try (JarInputStream inputStream = new JarInputStream(getData().getInputStream())) { return this.entries.getCertification(entry);
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();
}
}
} }
catch (IOException ex) { catch (IOException ex) {
throw new IllegalStateException(ex); throw new IllegalStateException(ex);
} }
} }
private void setCertificates(JarEntry entry, java.util.jar.JarEntry certEntry) {
if (entry != null) {
entry.setCertificates(certEntry);
}
}
public void clearCache() { public void clearCache() {
this.entries.clearCache(); this.entries.clearCache();
} }

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.NoSuchElementException;
import java.util.jar.Attributes; import java.util.jar.Attributes;
import java.util.jar.Attributes.Name; import java.util.jar.Attributes.Name;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest; import java.util.jar.Manifest;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
@ -91,14 +92,13 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
private Boolean multiReleaseJar; private Boolean multiReleaseJar;
private JarEntryCertification[] certifications;
private final Map<Integer, FileHeader> entriesCache = Collections private final Map<Integer, FileHeader> entriesCache = Collections
.synchronizedMap(new LinkedHashMap<Integer, FileHeader>(16, 0.75f, true) { .synchronizedMap(new LinkedHashMap<Integer, FileHeader>(16, 0.75f, true) {
@Override @Override
protected boolean removeEldestEntry(Map.Entry<Integer, FileHeader> eldest) { protected boolean removeEldestEntry(Map.Entry<Integer, FileHeader> eldest) {
if (JarFileEntries.this.jarFile.isSigned()) {
return false;
}
return size() >= ENTRY_CACHE_SIZE; return size() >= ENTRY_CACHE_SIZE;
} }
@ -313,7 +313,7 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
FileHeader entry = (cached != null) ? cached : CentralDirectoryFileHeader FileHeader entry = (cached != null) ? cached : CentralDirectoryFileHeader
.fromRandomAccessData(this.centralDirectoryData, this.centralDirectoryOffsets[index], this.filter); .fromRandomAccessData(this.centralDirectoryData, this.centralDirectoryOffsets[index], this.filter);
if (CentralDirectoryFileHeader.class.equals(entry.getClass()) && type.equals(JarEntry.class)) { 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) { if (cacheEntry && cached != entry) {
this.entriesCache.put(index, entry); this.entriesCache.put(index, entry);
@ -344,6 +344,41 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
return (this.filter != null) ? this.filter.apply(name) : name; 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. * Iterator for contained entries.
*/ */

@ -44,6 +44,7 @@ import org.springframework.boot.loader.TestJarCreator;
import org.springframework.boot.loader.data.RandomAccessDataFile; import org.springframework.boot.loader.data.RandomAccessDataFile;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.FileCopyUtils; import org.springframework.util.FileCopyUtils;
import org.springframework.util.StopWatch;
import org.springframework.util.StreamUtils; import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -375,29 +376,33 @@ public class JarFileTests {
@Test @Test
public void verifySignedJar() throws Exception { public void verifySignedJar() throws Exception {
String classpath = System.getProperty("java.class.path"); File signedJarFile = getSignedJarFile();
String[] entries = classpath.split(System.getProperty("path.separator")); assertThat(signedJarFile).exists();
String signedJarFile = null; try (java.util.jar.JarFile expected = new java.util.jar.JarFile(signedJarFile)) {
for (String entry : entries) { try (JarFile actual = new JarFile(signedJarFile)) {
if (entry.contains("bcprov")) { StopWatch stopWatch = new StopWatch();
signedJarFile = entry; Enumeration<JarEntry> 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(); private File getSignedJarFile() {
Enumeration<JarEntry> jarEntries = jarFile.entries(); String[] entries = System.getProperty("java.class.path").split(System.getProperty("path.separator"));
while (jarEntries.hasMoreElements()) { for (String entry : entries) {
JarEntry jarEntry = jarEntries.nextElement(); if (entry.contains("bcprov")) {
InputStream inputStream = jarFile.getInputStream(jarEntry); return new File(entry);
inputStream.skip(Long.MAX_VALUE);
inputStream.close();
if (!jarEntry.getName().startsWith("META-INF") && !jarEntry.isDirectory()
&& !jarEntry.getName().endsWith("TigerDigest.class")) {
assertThat(jarEntry.getCertificates()).isNotNull();
} }
} }
jarFile.close(); return null;
} }
@Test @Test

Loading…
Cancel
Save