Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<byte[]> 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);
Expand All @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1260,21 +1260,22 @@ public static Set<String> findClassNamesInJarFile(@Nullable File jarFile, boolea
}
Set<String> classNames;
try {
JarFile jarFile_ = new JarFile(jarFile);
Set<JarEntry> 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<JarEntry> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import io.microsphere.constants.ProtocolConstants;
import io.microsphere.filter.JarEntryFilter;
import io.microsphere.logging.Logger;
import io.microsphere.net.URLUtils;

Check warning on line 12 in microsphere-java-core/src/main/java/io/microsphere/util/jar/JarUtils.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import 'io.microsphere.net.URLUtils'.

See more on https://sonarcloud.io/project/issues?id=microsphere-projects_microsphere-java&issues=AZ5iGPWIw3y5MboyzFrY&open=AZ5iGPWIw3y5MboyzFrY&pullRequest=276
import io.microsphere.util.Utils;

import java.io.File;
Expand All @@ -30,13 +30,15 @@
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;
import static io.microsphere.net.URLUtils.decode;
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;
Expand All @@ -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.
Expand All @@ -77,10 +79,11 @@
*
* @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;
Expand All @@ -106,7 +109,7 @@
* 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);
Expand Down Expand Up @@ -244,13 +247,19 @@
*/
@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;
}

/**
Expand Down Expand Up @@ -305,8 +314,9 @@
* @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);
}
}

/**
Expand Down Expand Up @@ -366,23 +376,41 @@
* @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<JarEntry> 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<JarEntry> 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);
}
}


Expand Down Expand Up @@ -444,16 +472,22 @@
* @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<JarEntry> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
* @since 1.0.0
*/
@ParameterizedClass
@ValueSource(strings = {"INFO", "TRACE"})
@ValueSource(strings = {"TRACE", "INFO", "ERROR"})
@Disabled
public abstract class LoggingTest {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,36 @@
}

@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");

Check warning on line 149 in microsphere-java-core/src/test/java/io/microsphere/util/jar/JarUtilsTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename "resourceURL" which hides the field declared at line 60.

See more on https://sonarcloud.io/project/issues?id=microsphere-projects_microsphere-java&issues=AZ5iGPY7w3y5MboyzFrZ&open=AZ5iGPY7w3y5MboyzFrZ&pullRequest=276
assertDoesNotThrow(() -> extract(resourceURL, this.targetDirectory, null));
}

@Test
void testExtractOnJarEntryNotFound() {
String resource = this.resourceURL.toString();
URL resourceURL = ofURL(resource.replace("Nonnull.class", "NotFound.class"));

Check warning on line 156 in microsphere-java-core/src/test/java/io/microsphere/util/jar/JarUtilsTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename "resourceURL" which hides the field declared at line 60.

See more on https://sonarcloud.io/project/issues?id=microsphere-projects_microsphere-java&issues=AZ5iGPY7w3y5MboyzFra&open=AZ5iGPY7w3y5MboyzFra&pullRequest=276
assertDoesNotThrow(() -> extract(resourceURL, this.targetDirectory, null));
}

@Test
Expand Down
Loading
Loading