Align our jar URL stream handler with the JDK's

Previously our handler didn't override parseURL or sameFile which
resulted in behaviour that differed from that of the JDK's handler.
Crucially, this would result in our JarURLConnection being passed
a spec that didn't contain a "!/". A knock-on effect of this was
that the connection would point to the root of the jar rather than
the intended entry.

Closes gh-7021
pull/7262/head
Andy Wilkinson 8 years ago committed by Phillip Webb
parent ee7141cf63
commit d20ac56afd

@ -34,6 +34,7 @@ import java.util.logging.Logger;
* {@link URLStreamHandler} for Spring Boot loader {@link JarFile}s.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @see JarFile#registerUrlProtocolHandler()
*/
public class Handler extends URLStreamHandler {
@ -41,6 +42,8 @@ public class Handler extends URLStreamHandler {
// NOTE: in order to be found as a URL protocol handler, this class must be public,
// must be named Handler and must be in a package ending '.jar'
private static final String JAR_PROTOCOL = "jar:";
private static final String FILE_PROTOCOL = "file:";
private static final String SEPARATOR = "!/";
@ -140,6 +143,85 @@ public class Handler extends URLStreamHandler {
return (URLConnection) OPEN_CONNECTION_METHOD.invoke(handler, url);
}
@Override
protected void parseURL(URL context, String spec, int start, int limit) {
if (spec.toLowerCase().startsWith(JAR_PROTOCOL)) {
setFile(context, getFileFromSpec(spec.substring(start, limit)));
}
else {
setFile(context, getFileFromContext(context, spec.substring(start, limit)));
}
}
private String getFileFromSpec(String spec) {
int separatorIndex = spec.lastIndexOf("!/");
if (separatorIndex == -1) {
throw new IllegalArgumentException("No !/ in spec '" + spec + "'");
}
try {
new URL(spec.substring(0, separatorIndex));
return spec;
}
catch (MalformedURLException ex) {
throw new IllegalArgumentException("Invalid spec URL '" + spec + "'", ex);
}
}
private String getFileFromContext(URL context, String spec) {
String file = context.getFile();
if (spec.startsWith("/")) {
return trimToJarRoot(file) + SEPARATOR + spec.substring(1);
}
if (file.endsWith("/")) {
return file + spec;
}
int lastSlashIndex = file.lastIndexOf('/');
if (lastSlashIndex == -1) {
throw new IllegalArgumentException(
"No / found in context URL's file '" + file + "'");
}
return file.substring(0, lastSlashIndex + 1) + spec;
}
private String trimToJarRoot(String file) {
int lastSeparatorIndex = file.lastIndexOf(SEPARATOR);
if (lastSeparatorIndex == -1) {
throw new IllegalArgumentException(
"No !/ found in context URL's file '" + file + "'");
}
return file.substring(0, lastSeparatorIndex);
}
private void setFile(URL context, String file) {
setURL(context, JAR_PROTOCOL, null, -1, null, null, file, null, null);
}
@Override
protected boolean sameFile(URL u1, URL u2) {
if (!u1.getProtocol().equals("jar") || u2.getProtocol().equals("jar")) {
return super.sameFile(u1, u2);
}
int separator1 = u1.getFile().indexOf(SEPARATOR);
int separator2 = u1.getFile().indexOf(SEPARATOR);
if (separator1 < 0 || separator2 < 0) {
return super.sameFile(u1, u2);
}
String root1 = u1.getFile().substring(separator1 + SEPARATOR.length());
String root2 = u2.getFile().substring(separator2 + SEPARATOR.length());
if (!root1.equals(root2)) {
return super.sameFile(u1, u2);
}
String nested1 = u1.getFile().substring(0, separator1);
String nested2 = u1.getFile().substring(0, separator2);
try {
return super.sameFile(new URL(nested1), new URL(nested2));
}
catch (MalformedURLException ex) {
// Continue
}
return super.sameFile(u1, u2);
}
public JarFile getRootJarFileFromUrl(URL url) throws IOException {
String spec = url.getFile();
int separatorIndex = spec.indexOf(SEPARATOR);

@ -0,0 +1,96 @@
/*
* Copyright 2012-2016 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
*
* http://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.net.MalformedURLException;
import java.net.URL;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Handler}.
*
* @author Andy Wilkinson
*/
public class HandlerTests {
private final Handler handler = new Handler();
@Test
public void parseUrlWithJarRootContextAndAbsoluteSpecThatUsesContext()
throws MalformedURLException {
String spec = "/entry.txt";
URL context = createUrl("file:example.jar!/");
this.handler.parseURL(context, spec, 0, spec.length());
assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt");
}
@Test
public void parseUrlWithDirectoryEntryContextAndAbsoluteSpecThatUsesContext()
throws MalformedURLException {
String spec = "/entry.txt";
URL context = createUrl("file:example.jar!/dir/");
this.handler.parseURL(context, spec, 0, spec.length());
assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt");
}
@Test
public void parseUrlWithJarRootContextAndRelativeSpecThatUsesContext()
throws MalformedURLException {
String spec = "entry.txt";
URL context = createUrl("file:example.jar!/");
this.handler.parseURL(context, spec, 0, spec.length());
assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt");
}
@Test
public void parseUrlWithDirectoryEntryContextAndRelativeSpecThatUsesContext()
throws MalformedURLException {
String spec = "entry.txt";
URL context = createUrl("file:example.jar!/dir/");
this.handler.parseURL(context, spec, 0, spec.length());
assertThat(context.toExternalForm())
.isEqualTo("jar:file:example.jar!/dir/entry.txt");
}
@Test
public void parseUrlWithFileEntryContextAndRelativeSpecThatUsesContext()
throws MalformedURLException {
String spec = "entry.txt";
URL context = createUrl("file:example.jar!/dir/file");
this.handler.parseURL(context, spec, 0, spec.length());
assertThat(context.toExternalForm())
.isEqualTo("jar:file:example.jar!/dir/entry.txt");
}
@Test
public void parseUrlWithSpecThatIgnoresContext() throws MalformedURLException {
JarFile.registerUrlProtocolHandler();
String spec = "jar:file:/other.jar!/nested!/entry.txt";
URL context = createUrl("file:example.jar!/dir/file");
this.handler.parseURL(context, spec, 0, spec.length());
assertThat(context.toExternalForm())
.isEqualTo("jar:jar:file:/other.jar!/nested!/entry.txt");
}
private URL createUrl(String file) throws MalformedURLException {
return new URL("jar", null, -1, file, this.handler);
}
}

@ -18,7 +18,6 @@ package org.springframework.boot.loader.jar;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.net.URL;
import org.junit.Before;
@ -127,15 +126,14 @@ public class JarURLConnectionTests {
}
@Test
public void nestedJarNotFound() throws Exception {
URL url = new URL(
"jar:file:" + getAbsolutePath() + "!/nested.jar!/missing.jar!/1.dat");
public void connectionToEntryInNestedJarFromUrlThatUsesExistingUrlAsContext()
throws Exception {
URL url = new URL(new URL("jar", null, -1,
"file:" + getAbsolutePath() + "!/nested.jar!/", new Handler()), "/3.dat");
JarFile nested = this.jarFile
.getNestedJarFile(this.jarFile.getEntry("nested.jar"));
JarURLConnection connection = JarURLConnection.get(url, nested);
this.thrown.expect(FileNotFoundException.class);
this.thrown.expectMessage("JAR entry missing.jar not found in");
connection.connect();
assertThat(JarURLConnection.get(url, nested).getInputStream())
.hasSameContentAs(new ByteArrayInputStream(new byte[] { 3 }));
}
private String getAbsolutePath() {

Loading…
Cancel
Save