diff --git a/microsphere-java-core/src/main/java/io/microsphere/classloading/StreamArtifactResourceResolver.java b/microsphere-java-core/src/main/java/io/microsphere/classloading/StreamArtifactResourceResolver.java index 0b4312624..5fba8bc6d 100644 --- a/microsphere-java-core/src/main/java/io/microsphere/classloading/StreamArtifactResourceResolver.java +++ b/microsphere-java-core/src/main/java/io/microsphere/classloading/StreamArtifactResourceResolver.java @@ -17,6 +17,7 @@ package io.microsphere.classloading; import io.microsphere.annotation.Nullable; +import io.microsphere.io.FastByteArrayInputStream; import java.io.File; import java.io.FileInputStream; @@ -32,6 +33,7 @@ import static io.microsphere.collection.ListUtils.first; import static io.microsphere.io.FileUtils.resolveRelativePath; import static io.microsphere.io.IOUtils.close; +import static io.microsphere.io.IOUtils.toByteArray; import static io.microsphere.io.scanner.SimpleFileScanner.INSTANCE; import static io.microsphere.net.URLUtils.resolveArchiveFile; import static io.microsphere.util.Assert.assertNotNull; @@ -148,15 +150,19 @@ protected InputStream readArtifactMetadataDataFromArchiveFile(File archiveFile) @Nullable protected InputStream readArtifactMetadataDataFromFile(File archiveFile) throws IOException { - JarFile jarFile = new JarFile(archiveFile); - JarEntry jarEntry = findArtifactMetadataEntry(jarFile); - if (jarEntry == null) { - if (logger.isTraceEnabled()) { - logger.trace("The artifact metadata entry can't be resolved from the JarFile[path: '{}']", archiveFile); + try (JarFile jarFile = new JarFile(archiveFile)) { + JarEntry jarEntry = findArtifactMetadataEntry(jarFile); + if (jarEntry == null) { + if (logger.isTraceEnabled()) { + logger.trace("The artifact metadata entry can't be resolved from the JarFile[path: '{}']", archiveFile); + } + return null; + } + // Buffer the entire content so the JarFile can be closed safely + try (InputStream inputStream = jarFile.getInputStream(jarEntry)) { + return new FastByteArrayInputStream(toByteArray(inputStream)); } - return null; } - return jarFile.getInputStream(jarEntry); } @Nullable diff --git a/microsphere-java-core/src/main/java/io/microsphere/net/URLUtils.java b/microsphere-java-core/src/main/java/io/microsphere/net/URLUtils.java index e8387f49d..c1b98258d 100644 --- a/microsphere-java-core/src/main/java/io/microsphere/net/URLUtils.java +++ b/microsphere-java-core/src/main/java/io/microsphere/net/URLUtils.java @@ -7,6 +7,7 @@ import io.microsphere.annotation.Nonnull; import io.microsphere.annotation.Nullable; import io.microsphere.constants.SymbolConstants; +import io.microsphere.io.IOUtils; import io.microsphere.logging.Logger; import io.microsphere.util.ArrayUtils; import io.microsphere.util.Utils; @@ -666,8 +667,13 @@ public static boolean isJarURL(URL url) { String protocol = url.getProtocol(); boolean flag = false; if (FILE_PROTOCOL.equals(protocol)) { - JarFile jarFile = toJarFile(url); - flag = nonNull(jarFile); + JarFile jarFile = null; + try { + jarFile = toJarFile(url); + flag = nonNull(jarFile); + } finally { + IOUtils.close(jarFile); + } } else if (JAR_PROTOCOL.equals(protocol)) { flag = true; } diff --git a/microsphere-java-core/src/main/java/io/microsphere/process/ProcessExecutor.java b/microsphere-java-core/src/main/java/io/microsphere/process/ProcessExecutor.java index a21a695d2..f44a17c85 100644 --- a/microsphere-java-core/src/main/java/io/microsphere/process/ProcessExecutor.java +++ b/microsphere-java-core/src/main/java/io/microsphere/process/ProcessExecutor.java @@ -25,7 +25,6 @@ import static io.microsphere.util.ExceptionUtils.wrap; import static java.lang.Long.getLong; import static java.lang.Long.parseLong; -import static java.lang.Runtime.getRuntime; import static java.util.concurrent.Executors.newSingleThreadExecutor; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -90,8 +89,6 @@ public class ProcessExecutor { private final ProcessManager processManager = INSTANCE; - private final Runtime runtime = getRuntime(); - private final String options; private final String commandLine; @@ -152,17 +149,18 @@ public void execute(OutputStream outputStream, long timeoutInMilliseconds) throw public void execute(OutputStream outputStream, long timeout, TimeUnit timeUnit) throws IOException, TimeoutException { Future future = executor.submit(() -> { - Process process = runtime.exec(commandLine); + // Use ProcessBuilder with merged stderr so we only need to drain one stream, + // avoiding the classic stdout/stderr deadlock caused by sequential draining. + Process process = new ProcessBuilder(commandLine.split("\\s+")) + .redirectErrorStream(true) + .start(); InputStream processInputStream = process.getInputStream(); - InputStream processErrorInputStream = process.getErrorStream(); FastByteArrayOutputStream targetOutputStream = new FastByteArrayOutputStream(); int exitValue = -1; try { processManager.addUnfinishedProcess(process, options); - // Copy the standard input stream + // Copy the merged standard + error output stream copy(processInputStream, targetOutputStream); - // Copy the error input stream - copy(processErrorInputStream, targetOutputStream); // wait for the process being executed process.waitFor(timeout, timeUnit); @@ -185,10 +183,10 @@ public void execute(OutputStream outputStream, long timeout, TimeUnit timeUnit) try { byte[] bytes = future.get(timeout, timeUnit); copy(new FastByteArrayInputStream(bytes), outputStream); + } catch (TimeoutException e) { + future.cancel(true); + throw e; } catch (Exception e) { - if (e instanceof TimeoutException) { - throw (TimeoutException) e; - } throw wrap(e, IOException.class); } } diff --git a/microsphere-java-core/src/main/java/io/microsphere/util/ClassUtils.java b/microsphere-java-core/src/main/java/io/microsphere/util/ClassUtils.java index 4f2c8b315..a22ba0508 100644 --- a/microsphere-java-core/src/main/java/io/microsphere/util/ClassUtils.java +++ b/microsphere-java-core/src/main/java/io/microsphere/util/ClassUtils.java @@ -1260,21 +1260,22 @@ public static Set findClassNamesInJarFile(@Nullable File jarFile, boolea } Set classNames; try { - JarFile jarFile_ = new JarFile(jarFile); - Set jarEntries = INSTANCE.scan(jarFile_, recursive, ClassFileJarEntryFilter.INSTANCE); - int size = size(jarEntries); - if (size == 0) { - classNames = emptySet(); - } else { - classNames = newLinkedHashSet(size); - for (JarEntry jarEntry : jarEntries) { - String jarEntryName = jarEntry.getName(); - String className = resolveClassName(jarEntryName); - if (isNotBlank(className)) { - classNames.add(className); + try (JarFile jf = new JarFile(jarFile)) { + Set jarEntries = INSTANCE.scan(jf, recursive, ClassFileJarEntryFilter.INSTANCE); + int size = size(jarEntries); + if (size == 0) { + classNames = emptySet(); + } else { + classNames = newLinkedHashSet(size); + for (JarEntry jarEntry : jarEntries) { + String jarEntryName = jarEntry.getName(); + String className = resolveClassName(jarEntryName); + if (isNotBlank(className)) { + classNames.add(className); + } } + classNames = unmodifiableSet(classNames); } - classNames = unmodifiableSet(classNames); } } catch (Exception e) { classNames = emptySet(); diff --git a/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java b/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java index 233536bd1..cceb2ced5 100644 --- a/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java +++ b/microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java @@ -30,6 +30,7 @@ import static io.microsphere.constants.ProtocolConstants.FILE_PROTOCOL; import static io.microsphere.constants.ProtocolConstants.JAR_PROTOCOL; import static io.microsphere.constants.SeparatorConstants.ARCHIVE_ENTRY_SEPARATOR; +import static io.microsphere.constants.SeparatorConstants.FILE_SEPARATOR; import static io.microsphere.io.IOUtils.close; import static io.microsphere.io.IOUtils.copy; import static io.microsphere.logging.LoggerFactory.getLogger; @@ -37,6 +38,7 @@ import static io.microsphere.net.URLUtils.normalizePath; import static io.microsphere.net.URLUtils.resolveArchiveFile; import static io.microsphere.text.FormatUtils.format; +import static io.microsphere.util.Assert.assertNotNull; import static io.microsphere.util.StringUtils.EMPTY; import static io.microsphere.util.StringUtils.substringAfter; import static java.util.Collections.emptyList; @@ -52,7 +54,7 @@ */ public abstract class JarUtils implements Utils { - private static final Logger logger = getLogger(URLUtils.class); + private static final Logger logger = getLogger(JarUtils.class); /** * The resource path of Manifest file in JAR archive. @@ -77,10 +79,11 @@ public abstract class JarUtils implements Utils { * * @param jarURL the URL pointing to a JAR file or entry; must not be {@code null} * @return a new {@link JarFile} instance if resolved successfully, or {@code null} if resolution fails + * @throws NullPointerException if the provided {@code jarURL} is {@code null} * @throws IllegalArgumentException if the URL protocol is neither "jar" nor "file" */ @Nullable - public static JarFile toJarFile(URL jarURL) throws IllegalArgumentException { + public static JarFile toJarFile(URL jarURL) throws NullPointerException, IllegalArgumentException { final String jarAbsolutePath = resolveJarAbsolutePath(jarURL); if (jarAbsolutePath == null) { return null; @@ -106,7 +109,7 @@ public static JarFile toJarFile(URL jarURL) throws IllegalArgumentException { * file} */ protected static void assertJarURLProtocol(URL jarURL) throws NullPointerException, IllegalArgumentException { - final String protocol = jarURL.getProtocol(); //NPE check + final String protocol = jarURL.getProtocol(); // NPE check if (!JAR_PROTOCOL.equals(protocol) && !FILE_PROTOCOL.equals(protocol)) { String message = format("the protocol['{}'] of 'jarURL' is unsupported, except '{}' and '{}' ", protocol, JAR_PROTOCOL, FILE_PROTOCOL); throw new IllegalArgumentException(message); @@ -244,13 +247,19 @@ protected static List doFilter(Iterable jarEntries, JarEntry */ @Nullable public static JarEntry findJarEntry(URL jarURL) { - JarFile jarFile = toJarFile(jarURL); - if (jarFile == null) { - return null; + JarFile jarFile = null; + try { + jarFile = toJarFile(jarURL); + if (jarFile == null) { + return null; + } + final String relativePath = resolveRelativePath(jarURL); + // JarEntry is metadata-only (extends ZipEntry) and does not hold a reference back + // to the JarFile, so it is safe to close the JarFile after retrieving the entry. + return jarFile.getJarEntry(relativePath); + } finally { + close(jarFile); } - final String relativePath = resolveRelativePath(jarURL); - JarEntry jarEntry = jarFile.getJarEntry(relativePath); - return jarEntry; } /** @@ -305,8 +314,9 @@ public static void extract(File jarSourceFile, File targetDirectory) throws IOEx * @throws IOException if an I/O error occurs during extraction or if the provided file is not a valid JAR */ public static void extract(File jarSourceFile, File targetDirectory, JarEntryFilter jarEntryFilter) throws IOException { - final JarFile jarFile = new JarFile(jarSourceFile); - extract(jarFile, targetDirectory, jarEntryFilter); + try (JarFile jarFile = new JarFile(jarSourceFile)) { + extract(jarFile, targetDirectory, jarEntryFilter); + } } /** @@ -366,23 +376,41 @@ public static void extract(JarFile jarFile, File targetDirectory, JarEntryFilter * @param jarResourceURL the URL pointing to a resource within a JAR file; must not be {@code null} * @param targetDirectory the directory where the contents should be extracted; must not be {@code null} * @param jarEntryFilter an optional filter to determine which entries to extract; if {@code null}, all entries are extracted - * @throws IOException if an I/O error occurs during extraction or resolving the JAR resource + * @throws IllegalArgumentException if {@code jarResourceURL} or {@code targetDirectory} is {@code null} + * @throws IOException if an I/O error occurs during extraction or resolving the JAR resource */ - public static void extract(URL jarResourceURL, File targetDirectory, JarEntryFilter jarEntryFilter) throws IOException { + public static void extract(@Nonnull URL jarResourceURL, @Nonnull File targetDirectory, @Nullable JarEntryFilter jarEntryFilter) + throws NullPointerException, IOException { + assertNotNull(jarResourceURL, () -> "The 'jarResourceURL' argument must not be null"); + assertNotNull(targetDirectory, () -> "The 'targetDirectory' argument must not be null"); final JarFile jarFile = toJarFile(jarResourceURL); - final String relativePath = resolveRelativePath(jarResourceURL); - final JarEntry jarEntry = jarFile.getJarEntry(relativePath); - final boolean isDirectory = jarEntry.isDirectory(); - List jarEntriesList = filter(jarFile, entry -> { - String name = entry.getName(); - if (isDirectory && name.equals(relativePath)) { - return true; - } else return name.startsWith(relativePath); - }); + if (jarFile == null) { + if (logger.isWarnEnabled()) { + logger.warn("The provided URL does not point to a valid JAR resource: {}", jarResourceURL); + } + return; + } + try (JarFile jf = jarFile) { + final String relativePath = resolveRelativePath(jarResourceURL); + final JarEntry jarEntry = jf.getJarEntry(relativePath); + if (jarEntry == null) { + if (logger.isWarnEnabled()) { + logger.warn("The provided URL does not point to a valid JAR entry: {}", jarResourceURL); + } + return; + } + final boolean isDirectory = jarEntry.isDirectory(); + List jarEntriesList = filter(jf, entry -> { + String name = entry.getName(); + if (isDirectory && name.equals(relativePath)) { + return true; + } else return name.startsWith(relativePath); + }); - jarEntriesList = doFilter(jarEntriesList, jarEntryFilter); + jarEntriesList = doFilter(jarEntriesList, jarEntryFilter); - doExtract(jarFile, jarEntriesList, targetDirectory); + doExtract(jf, jarEntriesList, targetDirectory); + } } @@ -444,16 +472,22 @@ public static boolean isDirectoryEntry(URL url) { * @param jarFile the source JAR file; if {@code null}, no extraction is performed * @param jarEntries the collection of entries to extract; if empty or {@code null}, no extraction is performed * @param targetDirectory the target directory for extraction; must not be {@code null} - * @throws IOException if an I/O error occurs during extraction + * @throws NullPointerException if {@code targetDirectory} is {@code null} + * @throws IOException if an I/O error occurs during extraction * @since 1.0.0 */ protected static void doExtract(JarFile jarFile, Collection jarEntries, File targetDirectory) throws IOException { if (jarFile == null || isEmpty(jarEntries)) { return; } + final String targetCanonicalPath = targetDirectory.getCanonicalPath() + FILE_SEPARATOR; for (JarEntry jarEntry : jarEntries) { String jarEntryName = jarEntry.getName(); File targetFile = new File(targetDirectory, jarEntryName); + // Zip Slip protection: ensure the output file stays within the target directory + if (!targetFile.getCanonicalPath().startsWith(targetCanonicalPath)) { + throw new IOException("JAR entry '" + jarEntryName + "' would be extracted outside of the target directory"); + } if (jarEntry.isDirectory()) { targetFile.mkdirs(); } else { diff --git a/microsphere-java-core/src/test/java/io/microsphere/LoggingTest.java b/microsphere-java-core/src/test/java/io/microsphere/LoggingTest.java index 578b52537..c3c778241 100644 --- a/microsphere-java-core/src/test/java/io/microsphere/LoggingTest.java +++ b/microsphere-java-core/src/test/java/io/microsphere/LoggingTest.java @@ -38,7 +38,7 @@ * @since 1.0.0 */ @ParameterizedClass -@ValueSource(strings = {"INFO", "TRACE"}) +@ValueSource(strings = {"TRACE", "INFO", "ERROR"}) @Disabled public abstract class LoggingTest { diff --git a/microsphere-java-core/src/test/java/io/microsphere/util/jar/JarUtilsTest.java b/microsphere-java-core/src/test/java/io/microsphere/util/jar/JarUtilsTest.java index 11c03dea8..d4ec4e9c4 100644 --- a/microsphere-java-core/src/test/java/io/microsphere/util/jar/JarUtilsTest.java +++ b/microsphere-java-core/src/test/java/io/microsphere/util/jar/JarUtilsTest.java @@ -125,16 +125,36 @@ void testToJarFileOnNPE() { } @Test - void testFindJarEntry() throws Exception { + void testFindJarEntry() { URL resourceURL = getClassResource(this.classLoader, Nonnull.class); JarEntry jarEntry = findJarEntry(resourceURL); assertNotNull(jarEntry); } @Test - void testExtract() throws IOException { + void testExtract() { String jarAbsolutePath = resolveJarAbsolutePath(this.resourceURL); - extract(new File(jarAbsolutePath), this.targetDirectory); + assertDoesNotThrow(() -> extract(new File(jarAbsolutePath), this.targetDirectory)); + } + + @Test + void testExtractOnNull() { + assertThrows(IllegalArgumentException.class, () -> extract((URL) null, this.targetDirectory, null)); + assertThrows(IllegalArgumentException.class, () -> extract(this.resourceURL, null, null)); + assertDoesNotThrow(() -> extract(this.resourceURL, this.targetDirectory, null)); + } + + @Test + void testExtractOnJarFileNotFound() { + URL resourceURL = ofURL("jar:file:/path/to/file.jar!/entry"); + assertDoesNotThrow(() -> extract(resourceURL, this.targetDirectory, null)); + } + + @Test + void testExtractOnJarEntryNotFound() { + String resource = this.resourceURL.toString(); + URL resourceURL = ofURL(resource.replace("Nonnull.class", "NotFound.class")); + assertDoesNotThrow(() -> extract(resourceURL, this.targetDirectory, null)); } @Test diff --git a/microsphere-jdk-tools/src/main/java/io/microsphere/jdk/tools/compiler/Compiler.java b/microsphere-jdk-tools/src/main/java/io/microsphere/jdk/tools/compiler/Compiler.java index 09ab04336..50aa1588b 100644 --- a/microsphere-jdk-tools/src/main/java/io/microsphere/jdk/tools/compiler/Compiler.java +++ b/microsphere-jdk-tools/src/main/java/io/microsphere/jdk/tools/compiler/Compiler.java @@ -47,6 +47,7 @@ import static io.microsphere.logging.LoggerFactory.getLogger; import static io.microsphere.text.FormatUtils.format; import static io.microsphere.util.ArrayUtils.ofArray; +import static io.microsphere.util.Assert.assertNotNull; import static io.microsphere.util.ClassUtils.getTypeName; import static io.microsphere.util.StringUtils.substringBefore; import static java.io.File.separatorChar; @@ -102,6 +103,7 @@ public Compiler(File defaultSourceDirectory, File targetDirectory) { this.sourcePaths = newLinkedHashSet(defaultSourceDirectory); this.targetDirectory = targetDirectory; this.javaCompiler = getSystemJavaCompiler(); + assertNotNull(this.javaCompiler, () -> "No Java compiler available. Ensure this process is running on a JDK (not just a JRE)."); } public Compiler options(String... options) { @@ -151,11 +153,12 @@ public Compiler charset(Charset charset) { public boolean compile(Class... sourceClasses) throws IOException { JavaCompiler javaCompiler = getJavaCompiler(); - StandardJavaFileManager javaFileManager = getJavaFileManager(); - CompilationTask task = javaCompiler.getTask(null, javaFileManager, - getDiagnosticListener(), getOptions(), null, getJavaFileObjects(javaFileManager, sourceClasses)); - task.setProcessors(this.getProcessors()); - return task.call(); + try (StandardJavaFileManager javaFileManager = getJavaFileManager()) { + CompilationTask task = javaCompiler.getTask(null, javaFileManager, + getDiagnosticListener(), getOptions(), null, getJavaFileObjects(javaFileManager, sourceClasses)); + task.setProcessors(this.getProcessors()); + return task.call(); + } } public JavaCompiler getJavaCompiler() { diff --git a/microsphere-lang-model/src/main/java/io/microsphere/lang/model/util/ResolvableAnnotationValueVisitor.java b/microsphere-lang-model/src/main/java/io/microsphere/lang/model/util/ResolvableAnnotationValueVisitor.java index 86bce41b5..3b50ae1a9 100644 --- a/microsphere-lang-model/src/main/java/io/microsphere/lang/model/util/ResolvableAnnotationValueVisitor.java +++ b/microsphere-lang-model/src/main/java/io/microsphere/lang/model/util/ResolvableAnnotationValueVisitor.java @@ -164,7 +164,7 @@ public Object visitAnnotation(AnnotationMirror a, ExecutableElement attributeMet attributesMap.put(attributeName, attributeValue); } - if (nestedAnnotationsAsMap) { + if (nestedAnnotationsAsMap || annotationForMapMethod == null) { return attributesMap; }