Skip to content

Commit

Permalink
fix: Improve zip support for IOUtils (#7653)
Browse files Browse the repository at this point in the history
This patch moves IOUtils zip handling to use `FileSystems.newFileSystem(Path, ...)` API instead of the URI-based method. The URI-based FS is shared globally, so it can lead to race conditions when opening/closing file systems. The Path-based FS is unshared and can be used safely.

Additionally, this patch implements support for nested jar files. For Java 11+ this works with default zipfs, but earlier versions do not support zipfs nesting, so there's a fallback to extract the intermediate jar instead.

Resolves #7620
Resolves #7626
  • Loading branch information
yawkat authored Jul 11, 2022
1 parent 26a5273 commit 65c46ac
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 11 deletions.
60 changes: 49 additions & 11 deletions core/src/main/java/io/micronaut/core/io/IOUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.Reader;
import java.net.URI;
Expand All @@ -32,8 +33,11 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.nio.file.ProviderNotFoundException;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Stream;

Expand Down Expand Up @@ -76,18 +80,23 @@ public static void eachFile(@NonNull URL url, String path, @NonNull Consumer<Pat
@SuppressWarnings({"java:S2095", "java:S1141", "java:S3776"})
public static void eachFile(@NonNull URI uri, String path, @NonNull Consumer<Path> consumer) {
Path myPath;
List<Closeable> toClose = new ArrayList<>();
try {
String scheme = uri.getScheme();
FileSystem fileSystem = null;

try {
if ("jar".equals(scheme)) {
try {
fileSystem = FileSystems.getFileSystem(uri);
} catch (FileSystemNotFoundException e) {
fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap());
// try to match FileSystems.newFileSystem(URI) semantics for zipfs here.
// Basically ignores anything after the !/ if it exists, and uses the part
// before as the jar path to extract.
String jarUri = uri.getSchemeSpecificPart();
int sep = jarUri.lastIndexOf("!/");
if (sep != -1) {
jarUri = jarUri.substring(0, sep);
}
myPath = fileSystem.getPath(path);
// now, add the !/ at the end again so that loadNestedJarUri can handle it:
jarUri += "!/";
myPath = loadNestedJarUri(toClose, jarUri).resolve(path);
} else if ("file".equals(scheme)) {
myPath = Paths.get(uri).resolve(path);
} else {
Expand All @@ -107,15 +116,44 @@ public static void eachFile(@NonNull URI uri, String path, @NonNull Consumer<Pat
}
consumer.accept(currentPath);
}
} finally {
if (fileSystem != null && fileSystem.isOpen()) {
fileSystem.close();
}
}
}
} catch (IOException e) {
// ignore, can't do anything here and can't log because class used in compiler
} finally {
for (Closeable closeable : toClose) {
try {
closeable.close();
} catch (IOException ignored) {
}
}
}
}

private static Path loadNestedJarUri(List<Closeable> toClose, String jarUri) throws IOException {
int sep = jarUri.lastIndexOf("!/");
if (sep == -1) {
return Paths.get(URI.create(jarUri));
}
Path jarPath = loadNestedJarUri(toClose, jarUri.substring(0, sep));
FileSystem zipfs;
try {
// can't use newFileSystem(Path) here (without CL) because it doesn't exist on java 8
// the CL cast is necessary because since java 13 there is a newFileSystem(Path, Map)
zipfs = FileSystems.newFileSystem(jarPath, (ClassLoader) null);
toClose.add(0, zipfs);
} catch (ProviderNotFoundException e) {
// java versions earlier than 11 do not support nested zipfs and will fail with this
// exception. Try to extract the file instead. This is not efficient, but what else can
// we do?
Path tmp = Files.createTempFile("micronaut-IOUtils-nested-zip", ".zip");
toClose.add(0, () -> Files.deleteIfExists(tmp));
Files.copy(jarPath, tmp, StandardCopyOption.REPLACE_EXISTING);

zipfs = FileSystems.newFileSystem(tmp, (ClassLoader) null);
toClose.add(0, zipfs);
}
return zipfs.getPath(jarUri.substring(sep + 1));
}

/**
Expand Down
69 changes: 69 additions & 0 deletions core/src/test/groovy/io/micronaut/core/io/IOUtilsSpec.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.micronaut.core.io

import spock.lang.Specification

import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream

class IOUtilsSpec extends Specification {
def 'nested access to same zip file'() {
given:
Path zipPath = Files.createTempFile("micronaut-ioutils-spec", ".zip")
try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zipPath))) {
zos.putNextEntry(new ZipEntry("foo/bar.txt"))
zos.write("baz".getBytes(StandardCharsets.UTF_8))
zos.closeEntry()
}

def visitedOuter = []
def visitedInner = []

when:
IOUtils.eachFile(URI.create('jar:' + zipPath.toUri()), 'foo', entry -> {
visitedOuter.add(entry.getFileName().toString())
IOUtils.eachFile(URI.create('jar:' + zipPath.toUri()), 'foo', entryI -> {
visitedInner.add(entryI.getFileName().toString())
})
})
then:
visitedOuter == ['bar.txt']
visitedInner == ['bar.txt']

cleanup:
Files.deleteIfExists(zipPath)
}

def 'access to nested zip files'() {
given:
Path zipPath = Files.createTempFile("micronaut-ioutils-spec", ".zip")
try (ZipOutputStream outer = new ZipOutputStream(Files.newOutputStream(zipPath))) {
outer.putNextEntry(new ZipEntry("foo/inner.zip"))

ZipOutputStream inner = new ZipOutputStream(outer)
inner.putNextEntry(new ZipEntry("bar/baz.txt"))
inner.write("bla".getBytes(StandardCharsets.UTF_8))
inner.closeEntry()
inner.finish()

outer.closeEntry()
}

def visitedInner = []
def textInner = []

when:
IOUtils.eachFile(URI.create('jar:' + zipPath.toUri() + '!/foo/inner.zip!/xyz'), 'bar', entry -> {
visitedInner.add(entry.getFileName().toString())
textInner = Files.readAllLines(entry)
})
then:
visitedInner == ['baz.txt']
textInner == ['bla']

cleanup:
Files.deleteIfExists(zipPath)
}
}

0 comments on commit 65c46ac

Please sign in to comment.