Skip to content

Commit

Permalink
Allow FileSystems to be create by splitting URLs
Browse files Browse the repository at this point in the history
Relax the constraint that a `NestedLocation` must have a nested entry
name specified so that URLs can be split and rebuilt.

Prior to this commit, given a URL of the following form:

	jar:nested:/myjar.jar!/nested.jar!/my/file

It was possible to create a FileSystem from
"jar:nested:/myjar.jar!/nested.jar" and from that create a path to
"my/file".

However, it wasn't possible to create a FileSystem from
"jar:nested:/myjar.jar", then create another file system from the path
"nested.jar" and then finally create a path to "/nested.jar".

This was because `nested:/myjar.jar` was not considered a value URL
because it didn't include a nested entry name.

Projects such as `JobRunr` were relying on the ability to compose file
systems, so it makes sense to remove our somewhat artificial
restriction.

Fixes gh-38592
  • Loading branch information
philwebb committed Nov 29, 2023
1 parent 9a0f954 commit 6fd691a
Show file tree
Hide file tree
Showing 8 changed files with 64 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
import org.springframework.boot.loader.net.util.UrlDecoder;

/**
* A location obtained from a {@code nested:} {@link URL} consisting of a jar file and a
* nested entry.
* A location obtained from a {@code nested:} {@link URL} consisting of a jar file and an
* optional nested entry.
* <p>
* The syntax of a nested JAR URL is: <pre>
* nestedjar:&lt;path&gt;/!{entry}
Expand All @@ -54,13 +54,12 @@ public record NestedLocation(Path path, String nestedEntryName) {

private static final Map<String, NestedLocation> cache = new ConcurrentHashMap<>();

public NestedLocation {
public NestedLocation(Path path, String nestedEntryName) {
if (path == null) {
throw new IllegalArgumentException("'path' must not be null");
}
if (nestedEntryName == null || nestedEntryName.trim().isEmpty()) {
throw new IllegalArgumentException("'nestedEntryName' must not be empty");
}
this.path = path;
this.nestedEntryName = (nestedEntryName != null && !nestedEntryName.isEmpty()) ? nestedEntryName : null;
}

/**
Expand Down Expand Up @@ -94,20 +93,17 @@ static NestedLocation parse(String path) {
throw new IllegalArgumentException("'path' must not be empty");
}
int index = path.lastIndexOf("/!");
if (index == -1) {
throw new IllegalArgumentException("'path' must contain '/!'");
}
return cache.computeIfAbsent(path, (l) -> create(index, l));
}

private static NestedLocation create(int index, String location) {
String locationPath = location.substring(0, index);
String locationPath = (index != -1) ? location.substring(0, index) : location;
if (isWindows()) {
while (locationPath.startsWith("/")) {
locationPath = locationPath.substring(1, locationPath.length());
}
}
String nestedEntryName = location.substring(index + 2);
String nestedEntryName = (index != -1) ? location.substring(index + 2) : null;
return new NestedLocation((!locationPath.isEmpty()) ? Path.of(locationPath) : null, nestedEntryName);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ public Set<String> supportedFileAttributeViews() {
@Override
public Path getPath(String first, String... more) {
assertNotClosed();
if (first == null || first.isBlank() || more.length != 0) {
if (more.length != 0) {
throw new IllegalArgumentException("Nested paths must contain a single element");
}
return new NestedPath(this, first);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ final class NestedPath implements Path {
private volatile Boolean entryExists;

NestedPath(NestedFileSystem fileSystem, String nestedEntryName) {
if (fileSystem == null || nestedEntryName == null || nestedEntryName.isBlank()) {
throw new IllegalArgumentException("'filesSystem' and 'nestedEntryName' are required");
if (fileSystem == null) {
throw new IllegalArgumentException("'filesSystem' must not be null");
}
this.fileSystem = fileSystem;
this.nestedEntryName = nestedEntryName;
this.nestedEntryName = (nestedEntryName != null && !nestedEntryName.isBlank()) ? nestedEntryName : null;
}

Path getJarPath() {
Expand Down Expand Up @@ -138,8 +138,11 @@ public Path relativize(Path other) {
@Override
public URI toUri() {
try {
String jarFilePath = this.fileSystem.getJarPath().toUri().getPath();
return new URI("nested:" + jarFilePath + "/!" + this.nestedEntryName);
String uri = "nested:" + this.fileSystem.getJarPath().toUri().getPath();
if (this.nestedEntryName != null) {
uri += "/!" + this.nestedEntryName;
}
return new URI(uri);
}
catch (URISyntaxException ex) {
throw new IOError(ex);
Expand Down Expand Up @@ -187,7 +190,11 @@ public int hashCode() {

@Override
public String toString() {
return this.fileSystem.getJarPath() + this.fileSystem.getSeparator() + this.nestedEntryName;
String string = this.fileSystem.getJarPath().toString();
if (this.nestedEntryName != null) {
string += this.fileSystem.getSeparator() + this.nestedEntryName;
}
return string;
}

void assertExists() throws NoSuchFileException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,6 @@ void assertUrlIsNotMalformedWhenUrlIsNotNestedThrowsException() {
.withMessageContaining("must use 'nested'");
}

@Test
void assertUrlIsNotMalformedWhenUrlIsMalformedThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> Handler.assertUrlIsNotMalformed("nested:bad"))
.withMessageContaining("'path' must contain '/!'");
}

@Test
void assertUrlIsNotMalformedWhenUrlIsValidDoesNotThrowException() {
String url = "nested:" + this.temp.getAbsolutePath() + "/!nested.jar";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.nio.file.Path;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

Expand Down Expand Up @@ -52,15 +53,17 @@ void createWhenPathIsNullThrowsException() {
}

@Test
void createWhenNestedEntryNameIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(Path.of("test.jar"), null))
.withMessageContaining("'nestedEntryName' must not be empty");
void createWhenNestedEntryNameIsNull() {
NestedLocation location = new NestedLocation(Path.of("test.jar"), null);
assertThat(location.path().toString()).contains("test.jar");
assertThat(location.nestedEntryName()).isNull();
}

@Test
void createWhenNestedEntryNameIsEmptyThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(Path.of("test.jar"), null))
.withMessageContaining("'nestedEntryName' must not be empty");
void createWhenNestedEntryNameIsEmpty() {
NestedLocation location = new NestedLocation(Path.of("test.jar"), "");
assertThat(location.path().toString()).contains("test.jar");
assertThat(location.nestedEntryName()).isNull();
}

@Test
Expand All @@ -82,10 +85,11 @@ void fromUrlWhenNoPathThrowsException() {
}

@Test
void fromUrlWhenNoSeparatorThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> NestedLocation.fromUrl(new URL("nested:test.jar!nested.jar")))
.withMessageContaining("'path' must contain '/!'");
void fromUrlWhenNoSeparator() throws Exception {
File file = new File(this.temp, "test.jar");
NestedLocation location = NestedLocation.fromUrl(new URL("nested:" + file.getAbsolutePath() + "/"));
assertThat(location.path()).isEqualTo(file.toPath());
assertThat(location.nestedEntryName()).isNull();
}

@Test
Expand All @@ -110,10 +114,11 @@ void fromUriWhenNotNestedProtocolThrowsException() {
}

@Test
void fromUriWhenNoSeparatorThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> NestedLocation.fromUri(new URI("nested:test.jar!nested.jar")))
.withMessageContaining("'path' must contain '/!'");
@Disabled
void fromUriWhenNoSeparator() throws Exception {
NestedLocation location = NestedLocation.fromUri(new URI("nested:test.jar!nested.jar"));
assertThat(location.path().toString()).contains("test.jar!nested.jar");
assertThat(location.nestedEntryName()).isNull();
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.Cleaner.Cleanable;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.security.Permission;
Expand All @@ -41,7 +40,6 @@
import org.springframework.boot.loader.zip.ZipContent;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
Expand Down Expand Up @@ -74,13 +72,6 @@ void setup() throws Exception {
this.url = new URL("nested:" + this.jarFile.getAbsolutePath() + "/!nested.jar");
}

@Test
void createWhenMalformedUrlThrowsException() throws Exception {
URL url = new URL("nested:bad.jar");
assertThatExceptionOfType(MalformedURLException.class).isThrownBy(() -> new NestedUrlConnection(url))
.withMessage("'path' must contain '/!'");
}

@Test
void getContentLengthWhenContentLengthMoreThanMaxIntReturnsMinusOne() {
NestedUrlConnection connection = mock(NestedUrlConnection.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,14 @@ void getPathWhenClosedThrowsException() throws Exception {

@Test
void getPathWhenFirstIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.fileSystem.getPath(null))
.withMessage("Nested paths must contain a single element");
Path path = this.fileSystem.getPath(null);
assertThat(path.toString()).endsWith("/test.jar");
}

@Test
void getPathWhenFirstIsBlankThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.fileSystem.getPath(""))
.withMessage("Nested paths must contain a single element");
void getPathWhenFirstIsBlank() {
Path path = this.fileSystem.getPath("");
assertThat(path.toString()).endsWith("/test.jar");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
Expand Down Expand Up @@ -74,4 +75,22 @@ void nestedZipWithoutNewFileSystem() throws Exception {
assertThat(Files.readAllBytes(path)).containsExactly(0x3);
}

@Test // gh-38592
void nestedZipSplitAndRestore() throws Exception {
File file = new File(this.temp, "test.jar");
TestJar.create(file);
URI uri = JarUrl.create(file, "nested.jar", "3.dat").toURI();
String[] components = uri.toString().split("!");
System.out.println(List.of(components));
try (FileSystem rootFs = FileSystems.newFileSystem(URI.create(components[0]), Collections.emptyMap())) {
Path childPath = rootFs.getPath(components[1]);
try (FileSystem childFs = FileSystems.newFileSystem(childPath)) {
Path nestedRoot = childFs.getPath("/");
assertThat(Files.list(nestedRoot)).hasSize(4);
Path path = childFs.getPath(components[2]);
assertThat(Files.readAllBytes(path)).containsExactly(0x3);
}
}
}

}

0 comments on commit 6fd691a

Please sign in to comment.