From d20ac56afdf214f34479534606d9f1007437abf6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 26 Sep 2016 16:04:57 +0100 Subject: [PATCH] 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 --- .../boot/loader/jar/Handler.java | 82 ++++++++++++++++ .../boot/loader/jar/HandlerTests.java | 96 +++++++++++++++++++ .../loader/jar/JarURLConnectionTests.java | 14 ++- 3 files changed, 184 insertions(+), 8 deletions(-) create mode 100644 spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java index 4637c65a32..d4c1841656 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java @@ -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); diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java new file mode 100644 index 0000000000..8b5dbe31b1 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java @@ -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); + } + +} diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java index 32a38cb2ee..ef98792d9f 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java @@ -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() {