From 65c46ac6ffcbf45b2673b0e220c30a1c920bb1a4 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Mon, 11 Jul 2022 04:12:25 +0200 Subject: [PATCH] fix: Improve zip support for IOUtils (#7653) 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 --- .../java/io/micronaut/core/io/IOUtils.java | 60 +++++++++++++--- .../io/micronaut/core/io/IOUtilsSpec.groovy | 69 +++++++++++++++++++ 2 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 core/src/test/groovy/io/micronaut/core/io/IOUtilsSpec.groovy diff --git a/core/src/main/java/io/micronaut/core/io/IOUtils.java b/core/src/main/java/io/micronaut/core/io/IOUtils.java index 2150177b8da..32e2d443512 100644 --- a/core/src/main/java/io/micronaut/core/io/IOUtils.java +++ b/core/src/main/java/io/micronaut/core/io/IOUtils.java @@ -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; @@ -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; @@ -76,18 +80,23 @@ public static void eachFile(@NonNull URL url, String path, @NonNull Consumer consumer) { Path myPath; + List 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 { @@ -107,15 +116,44 @@ public static void eachFile(@NonNull URI uri, String path, @NonNull Consumer 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)); } /** diff --git a/core/src/test/groovy/io/micronaut/core/io/IOUtilsSpec.groovy b/core/src/test/groovy/io/micronaut/core/io/IOUtilsSpec.groovy new file mode 100644 index 00000000000..960868ca775 --- /dev/null +++ b/core/src/test/groovy/io/micronaut/core/io/IOUtilsSpec.groovy @@ -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) + } +}