Skip to content

Commit

Permalink
Merge pull request spring-projects#19041 from mathieufortin01
Browse files Browse the repository at this point in the history
* pr/19041:
  Polish 'Fix signed jar performance issues'
  Fix signed jar performance issues
  Ignore Visual Studio Code Files

Closes spring-projectsgh-19041
  • Loading branch information
philwebb committed Sep 14, 2020
2 parents b3960ca + c6a9696 commit 895ff9c
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 63 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Expand Up @@ -19,6 +19,7 @@
.settings
.springBeans
/build
.vscode
/code
MANIFEST.MF
_site/
Expand All @@ -38,4 +39,4 @@ transaction-logs
secrets.yml
.gradletasknamecache
.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");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -32,20 +32,21 @@
*/
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;
Expand All @@ -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;
}
Expand All @@ -85,23 +90,24 @@ public Attributes getAttributes() throws IOException {

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

}
Expand Up @@ -27,7 +27,6 @@
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;

Expand Down Expand Up @@ -387,33 +386,15 @@ boolean isSigned() {
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();
}
Expand Down
@@ -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.
Expand All @@ -26,6 +26,7 @@
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;

Expand Down Expand Up @@ -91,14 +92,13 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {

private Boolean multiReleaseJar;

private JarEntryCertification[] certifications;

private final Map<Integer, FileHeader> entriesCache = Collections
.synchronizedMap(new LinkedHashMap<Integer, FileHeader>(16, 0.75f, true) {

@Override
protected boolean removeEldestEntry(Map.Entry<Integer, FileHeader> eldest) {
if (JarFileEntries.this.jarFile.isSigned()) {
return false;
}
return size() >= ENTRY_CACHE_SIZE;
}

Expand Down Expand Up @@ -313,7 +313,7 @@ private <T extends FileHeader> T getEntry(int index, Class<T> type, boolean cach
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);
Expand Down Expand Up @@ -344,6 +344,41 @@ private AsciiBytes applyFilter(AsciiBytes 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.
*/
Expand Down
Expand Up @@ -44,6 +44,7 @@
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;
Expand Down Expand Up @@ -375,29 +376,33 @@ public void sensibleToString() throws Exception {

@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<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();
Enumeration<JarEntry> 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
Expand Down

0 comments on commit 895ff9c

Please sign in to comment.