
io.github.classgraph.ClasspathElementZip Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of classgraph Show documentation
Show all versions of classgraph Show documentation
The uber-fast, ultra-lightweight classpath and module scanner for JVM languages.
/*
* This file is part of ClassGraph.
*
* Author: Luke Hutchison
*
* Hosted at: https://github.com/lukehutch/fast-classpath-scanner
*
* --
*
* The MIT License (MIT)
*
* Copyright (c) 2018 Luke Hutchison
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without
* limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial
* portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
* EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.classgraph;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import io.github.classgraph.ScanSpec.ScanSpecPathMatch;
import io.github.classgraph.utils.ClasspathOrModulePathEntry;
import io.github.classgraph.utils.FastPathResolver;
import io.github.classgraph.utils.FileUtils;
import io.github.classgraph.utils.InputStreamOrByteBufferAdapter;
import io.github.classgraph.utils.JarfileMetadataReader;
import io.github.classgraph.utils.LogNode;
import io.github.classgraph.utils.NestedJarHandler;
import io.github.classgraph.utils.Recycler;
import io.github.classgraph.utils.URLPathEncoder;
import io.github.classgraph.utils.WorkQueue;
/** A zip/jarfile classpath element. */
class ClasspathElementZip extends ClasspathElement {
private File classpathEltZipFile;
/** Result of parsing the manifest file for this jarfile. */
private JarfileMetadataReader jarfileMetadataReader;
/** The package root within the jarfile. */
private String packageRootPrefix;
private Recycler zipFileRecycler;
/** A zip/jarfile classpath element. */
ClasspathElementZip(final ClasspathOrModulePathEntry classpathEltPath, final ScanSpec scanSpec,
final NestedJarHandler nestedJarHandler, final WorkQueue workQueue,
final LogNode log) {
super(classpathEltPath, scanSpec);
try {
classpathEltZipFile = classpathEltPath.getFile(log);
} catch (final IOException e) {
if (log != null) {
log.log("Exception while trying to canonicalize path " + classpathEltPath.getResolvedPath(), e);
}
skipClasspathElement = true;
return;
}
if (classpathEltZipFile == null || !FileUtils.canRead(classpathEltZipFile)) {
if (log != null) {
log.log("Skipping non-existent jarfile " + classpathEltPath.getResolvedPath());
}
skipClasspathElement = true;
return;
}
try {
classpathEltPath.getCanonicalPath(log);
} catch (final Exception e) {
if (log != null) {
log.log("Skipping jarfile " + classpathEltPath.getResolvedPath()
+ " -- could not canonicalize path : " + e);
}
skipClasspathElement = true;
return;
}
try {
zipFileRecycler = nestedJarHandler.getZipFileRecycler(classpathEltZipFile, log);
} catch (final Exception e) {
if (log != null) {
log.log("Exception while creating zipfile recycler for " + classpathEltZipFile + " : " + e);
}
skipClasspathElement = true;
return;
}
final String packageRoot = classpathEltPath.getJarfilePackageRoot();
try {
jarfileMetadataReader = nestedJarHandler.getJarfileMetadataReader(classpathEltZipFile, packageRoot,
log);
} catch (final Exception e) {
if (log != null) {
log.log("Exception while reading metadata from " + classpathEltZipFile + " : " + e);
}
skipClasspathElement = true;
return;
}
if (jarfileMetadataReader == null) {
skipClasspathElement = true;
return;
}
if (!packageRoot.isEmpty()) {
if (log != null) {
log.log("Package root within jarfile: " + packageRoot);
}
packageRootPrefix = packageRoot + "/";
} else {
packageRootPrefix = "";
}
while (packageRootPrefix.startsWith("/")) {
// Strip any initial "/" to correspond with handling of relativePath below
packageRootPrefix = packageRootPrefix.substring(1);
}
// Parse the manifest entry if present
if (jarfileMetadataReader != null && jarfileMetadataReader.classPathEntriesToScan != null) {
// Class-Path entries in the manifest file are resolved relative to the dir the manifest's jarfile
// is contaiin. Get the parent path.
final String pathOfContainingDir = FastPathResolver.resolve(classpathEltZipFile.getParent());
// Create child classpath elements from Class-Path entry
if (childClasspathElts == null) {
childClasspathElts = new ArrayList<>(jarfileMetadataReader.classPathEntriesToScan.size());
}
for (int i = 0; i < jarfileMetadataReader.classPathEntriesToScan.size(); i++) {
final String childClassPathEltPath = jarfileMetadataReader.classPathEntriesToScan.get(i);
final ClasspathOrModulePathEntry childRelativePath = new ClasspathOrModulePathEntry(
pathOfContainingDir, childClassPathEltPath, classpathEltPath.getClassLoaders(),
nestedJarHandler, scanSpec, log);
if (!childRelativePath.equals(classpathEltPath)) {
// Add child classpath element. This may add lib jars more than once, in the case of a
// jar with "BOOT-INF/classes" and "BOOT-INF/lib", since this method may be called initially
// with "" as the package root, and then a second time with "BOOT-INF/classes" as a package
// root, and both times it will find "BOOT-INF/lib" -- but the caller will deduplicate
// the multiply-added lib jars.
childClasspathElts.add(childRelativePath);
}
}
// Schedule child classpath elements for scanning
if (!childClasspathElts.isEmpty()) {
if (workQueue != null) {
workQueue.addWorkUnits(childClasspathElts);
} else {
// When adding rt.jar, workQueue will be null. But rt.jar should not include Class-Path
// references (so this block should not be reached).
if (log != null) {
log.log("Ignoring Class-Path entries in rt.jar: " + childClasspathElts);
}
}
}
}
if (scanSpec.performScan) {
fileMatches = new ArrayList<>();
classfileMatches = new ArrayList<>();
fileToLastModified = new HashMap<>();
}
}
private Resource newClasspathResource(final File jarFile, final String packageRootPrefix,
final String pathRelativeToPackageRoot, final ZipEntry zipEntry) {
return new Resource() {
private Recycler.Recyclable zipFileRecyclable;
private ZipFile zipFile;
private String pathRelativeToClasspathElt = null;
{
// ZipEntry size may be unknown (-1L), or even completely wrong
length = zipEntry.getSize();
}
@Override
public String getPath() {
return pathRelativeToPackageRoot;
}
@Override
public String getPathRelativeToClasspathElement() {
return pathRelativeToClasspathElt == null
? pathRelativeToClasspathElt = packageRootPrefix + pathRelativeToPackageRoot
: pathRelativeToClasspathElt;
}
@Override
public URL getURL() {
try {
return new URL(jarFile.toURI().toURL().toString() + "!"
+ URLPathEncoder.encodePath(getPathRelativeToClasspathElement()));
} catch (final MalformedURLException e) {
throw new IllegalArgumentException("Could not form URL for jarfile: " + jarFile + " ; path: "
+ pathRelativeToClasspathElt);
}
}
@Override
public URL getClasspathElementURL() {
try {
return new URL(
"jar:" + getClasspathElementFile().toURI().toURL() + (packageRootPrefix.isEmpty() ? ""
: "!" + URLPathEncoder.encodePath(packageRootPrefix)));
} catch (final MalformedURLException e) {
// Shouldn't happen
throw new IllegalArgumentException(e);
}
}
@Override
public File getClasspathElementFile() {
return classpathEltZipFile;
}
@Override
public ModuleRef getModuleRef() {
return null;
}
@Override
public InputStream open() throws IOException {
if (skipClasspathElement) {
// Shouldn't happen
throw new IOException("Jarfile could not be opened");
}
if (inputStream != null) {
throw new IllegalArgumentException(
"Resource is already open -- cannot open it again without first calling close()");
} else {
try {
zipFileRecyclable = zipFileRecycler.acquire();
zipFile = zipFileRecyclable.get();
inputStream = new InputStreamResourceCloser(this, zipFile.getInputStream(zipEntry));
length = zipEntry.getSize();
return inputStream;
} catch (final Exception e) {
close();
throw new IOException("Could not open " + this, e);
}
}
}
@Override
InputStreamOrByteBufferAdapter openOrRead() throws IOException {
return InputStreamOrByteBufferAdapter.create(open());
}
@Override
public ByteBuffer read() throws IOException {
open();
return inputStreamToByteBuffer();
}
@Override
public byte[] load() throws IOException {
try {
open();
final byte[] byteArray = inputStreamToByteArray();
length = byteArray.length;
return byteArray;
} finally {
close();
}
}
@Override
public void close() {
if (inputStream != null) {
try {
// Avoid infinite loop with InputStreamResourceCloser trying to close its parent resource
final InputStream inputStreamWrapper = inputStream;
inputStream = null;
inputStreamWrapper.close();
} catch (final IOException e) {
// Ignore
}
}
if (byteBuffer != null) {
byteBuffer = null;
}
if (zipFile != null) {
// Leave the ZipFile open in the recycler, just set the ref to null here.
// The open ZipFile instances are closed when ClasspathElementZip#close() is called.
zipFile = null;
}
if (zipFileRecyclable != null) {
// Recycle the (open) ZipFile
zipFileRecyclable.close();
zipFileRecyclable = null;
}
}
@Override
protected String toStringImpl() {
return "[jar " + jarFile + "]/" + getPathRelativeToClasspathElement();
}
};
}
/** Scan for path matches within jarfile, and record ZipEntry objects of matching files. */
@Override
void scanPaths(final LogNode log) {
if (jarfileMetadataReader == null) {
skipClasspathElement = true;
return;
}
final LogNode subLog = log == null ? null
: log.log(classpathEltPath.getResolvedPath(), "Scanning jarfile " + classpathEltPath);
// Support specification of a classpath root within a jarfile, e.g. "spring-project.jar!/BOOT-INF/classes"
final int requiredPrefixLen = packageRootPrefix.length();
Set loggedNestedClasspathRootPrefixes = null;
String prevParentRelativePath = null;
ScanSpecPathMatch prevParentMatchStatus = null;
for (final ZipEntry zipEntry : jarfileMetadataReader.zipEntries) {
// Normalize path of ZipEntry
String relativePath = zipEntry.getName();
while (relativePath.startsWith("/")) {
relativePath = relativePath.substring(1);
}
// Ignore entries without the correct classpath root prefix
if (requiredPrefixLen > 0) {
if (!relativePath.startsWith(packageRootPrefix)) {
continue;
}
// Strip the classpath root prefix from the relative path
relativePath = relativePath.substring(requiredPrefixLen);
}
// Check if the relative path is within a nested classpath root
if (nestedClasspathRootPrefixes != null) {
// This is O(mn), which is inefficient, but the number of nested classpath roots should be small
boolean reachedNestedRoot = false;
for (final String nestedClasspathRoot : nestedClasspathRootPrefixes) {
if (relativePath.startsWith(nestedClasspathRoot)) {
// relativePath has a prefix of nestedClasspathRoot
if (subLog != null) {
if (loggedNestedClasspathRootPrefixes == null) {
loggedNestedClasspathRootPrefixes = new HashSet<>();
}
if (loggedNestedClasspathRootPrefixes.add(nestedClasspathRoot)) {
subLog.log("Reached nested classpath root, stopping recursion to avoid duplicate "
+ "scanning: " + nestedClasspathRoot);
}
}
reachedNestedRoot = true;
break;
}
}
if (reachedNestedRoot) {
continue;
}
}
// Get match status of the parent directory of this zipentry file's relative path (or reuse the last
// match status for speed, if the directory name hasn't changed).
final int lastSlashIdx = relativePath.lastIndexOf("/");
final String parentRelativePath = lastSlashIdx < 0 ? "/" : relativePath.substring(0, lastSlashIdx + 1);
final boolean parentRelativePathChanged = prevParentRelativePath == null
|| !parentRelativePath.equals(prevParentRelativePath);
final ScanSpecPathMatch parentMatchStatus = //
parentRelativePathChanged ? scanSpec.dirWhitelistMatchStatus(parentRelativePath)
: prevParentMatchStatus;
prevParentRelativePath = parentRelativePath;
prevParentMatchStatus = parentMatchStatus;
// Class can only be scanned if it's within a whitelisted path subtree, or if it is a classfile that has
// been specifically-whitelisted
if (parentMatchStatus != ScanSpecPathMatch.HAS_WHITELISTED_PATH_PREFIX
&& parentMatchStatus != ScanSpecPathMatch.AT_WHITELISTED_PATH
&& (parentMatchStatus != ScanSpecPathMatch.AT_WHITELISTED_CLASS_PACKAGE
|| !scanSpec.isSpecificallyWhitelistedClass(relativePath))) {
if (subLog != null) {
subLog.log("Skipping non-whitelisted path: " + relativePath);
}
continue;
}
if (subLog != null) {
subLog.log(relativePath, "Found whitelisted file: " + relativePath);
}
if (scanSpec.enableClassInfo) {
// Store relative paths of any classfiles encountered
if (FileUtils.isClassfile(relativePath)) {
classfileMatches.add(
newClasspathResource(classpathEltZipFile, packageRootPrefix, relativePath, zipEntry));
}
}
// Record all classpath resources found in whitelisted paths
fileMatches.add(newClasspathResource(classpathEltZipFile, packageRootPrefix, relativePath, zipEntry));
}
// Don't use the last modified time from the individual zipEntry
// objects, we use the last modified time for the zipfile itself instead.
fileToLastModified.put(classpathEltZipFile, classpathEltZipFile.lastModified());
if (subLog != null) {
subLog.addElapsedTime();
}
}
/** Close and free all open ZipFiles. */
@Override
void closeRecyclers() {
if (zipFileRecycler != null) {
// Close the open ZipFile instances for this classpath element.
zipFileRecycler.close();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy