blob: a6c79ce7cb7499eb3e800aa3aa2dcb975bedafbd [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.vfs2.impl;
import java.io.IOException;
import java.net.URL;
import java.security.CodeSource;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.SecureClassLoader;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.vfs2.FileSystemManager;
import org.apache.commons.vfs2.NameScope;
import org.apache.commons.vfs2.util.FileObjectUtils;
/**
* A class loader that can load classes and resources from a search path.
* <p>
* The search path can consist of VFS FileObjects referring both to folders and JAR files. Any FileObject of type
* FileType.FILE is assumed to be a JAR and is opened by creating a layered file system with the "jar" scheme.
* </p>
* <p>
* TODO - Test this with signed Jars and a SecurityManager.
* </p>
*
* @see FileSystemManager#createFileSystem
*/
public class VFSClassLoader extends SecureClassLoader {
private final ArrayList<FileObject> resources = new ArrayList<>();
/**
* Constructors a new VFSClassLoader for the given file.
*
* @param file the file to load the classes and resources from.
* @param manager the FileManager to use when trying to create a layered Jar file system.
* @throws FileSystemException if an error occurs.
*/
public VFSClassLoader(final FileObject file, final FileSystemManager manager) throws FileSystemException {
this(new FileObject[] {file}, manager, null);
}
/**
* Constructors a new VFSClassLoader for the given file.
*
* @param file the file to load the classes and resources from.
* @param manager the FileManager to use when trying to create a layered Jar file system.
* @param parent the parent class loader for delegation.
* @throws FileSystemException if an error occurs.
*/
public VFSClassLoader(final FileObject file, final FileSystemManager manager, final ClassLoader parent)
throws FileSystemException {
this(new FileObject[] {file}, manager, parent);
}
/**
* Constructors a new VFSClassLoader for the given files. The files will be searched in the order specified.
*
* @param files the files to load the classes and resources from.
* @param manager the FileManager to use when trying to create a layered Jar file system.
* @throws FileSystemException if an error occurs.
*/
public VFSClassLoader(final FileObject[] files, final FileSystemManager manager) throws FileSystemException {
this(files, manager, null);
}
/**
* Constructors a new VFSClassLoader for the given FileObjects. The FileObjects will be searched in the order
* specified.
*
* @param files the FileObjects to load the classes and resources from.
* @param manager the FileManager to use when trying to create a layered Jar file system.
* @param parent the parent class loader for delegation.
* @throws FileSystemException if an error occurs.
*/
public VFSClassLoader(final FileObject[] files, final FileSystemManager manager, final ClassLoader parent)
throws FileSystemException {
super(parent);
addFileObjects(manager, files);
}
/**
* Appends the specified FileObjects to the list of FileObjects to search for classes and resources.
*
* @param manager The FileSystemManager.
* @param files the FileObjects to append to the search path.
* @throws FileSystemException if an error occurs.
*/
private void addFileObjects(final FileSystemManager manager, final FileObject[] files) throws FileSystemException {
for (FileObject file : files) {
if (!FileObjectUtils.exists(file)) {
// Does not exist - skip
continue;
}
// TODO - use federation instead
if (manager.canCreateFileSystem(file)) {
// Use contents of the file
file = manager.createFileSystem(file);
}
resources.add(file);
}
}
/**
* Copies the permissions from src to dest.
*
* @param src The source PermissionCollection.
* @param dest The destination PermissionCollection.
*/
protected void copyPermissions(final PermissionCollection src, final PermissionCollection dest) {
for (final Enumeration<Permission> elem = src.elements(); elem.hasMoreElements();) {
dest.add(elem.nextElement());
}
}
/**
* Loads and verifies the class with name and located with res.
*/
private Class<?> defineClass(final String name, final Resource res) throws IOException {
final URL url = res.getCodeSourceURL();
final String pkgName = res.getPackageName();
if (pkgName != null) {
final Package pkg = getPackage(pkgName);
if (pkg != null) {
if (pkg.isSealed()) {
if (!pkg.isSealed(url)) {
throw new FileSystemException("vfs.impl/pkg-sealed-other-url", pkgName);
}
} else if (isSealed(res)) {
throw new FileSystemException("vfs.impl/pkg-sealing-unsealed", pkgName);
}
} else {
definePackage(pkgName, res);
}
}
final byte[] bytes = res.getBytes();
final Certificate[] certs = res.getFileObject().getContent().getCertificates();
final CodeSource cs = new CodeSource(url, certs);
return defineClass(name, bytes, 0, bytes.length, cs);
}
/**
* Reads attributes for the package and defines it.
*/
private Package definePackage(final String name, final Resource res) throws FileSystemException {
// TODO - check for MANIFEST_ATTRIBUTES capability first
final String specTitle = res.getPackageAttribute(Name.SPECIFICATION_TITLE);
final String specVendor = res.getPackageAttribute(Attributes.Name.SPECIFICATION_VENDOR);
final String specVersion = res.getPackageAttribute(Name.SPECIFICATION_VERSION);
final String implTitle = res.getPackageAttribute(Name.IMPLEMENTATION_TITLE);
final String implVendor = res.getPackageAttribute(Name.IMPLEMENTATION_VENDOR);
final String implVersion = res.getPackageAttribute(Name.IMPLEMENTATION_VERSION);
final URL sealBase;
if (isSealed(res)) {
sealBase = res.getCodeSourceURL();
} else {
sealBase = null;
}
return definePackage(name, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor, sealBase);
}
/**
* Finds and loads the class with the specified name from the search path.
*
* @throws ClassNotFoundException if the class is not found.
*/
@Override
protected Class<?> findClass(final String name) throws ClassNotFoundException {
try {
final String path = name.replace('.', '/').concat(".class");
final Resource res = loadResource(path);
if (res == null) {
throw new ClassNotFoundException(name);
}
return defineClass(name, res);
} catch (final IOException ioe) {
throw new ClassNotFoundException(name, ioe);
}
}
/**
* Finds the resource with the specified name from the search path. This returns null if the resource is not found.
*
* @param name The resource name.
* @return The URL that matches the resource.
*/
@Override
protected URL findResource(final String name) {
try {
final Resource res = loadResource(name);
if (res != null) {
return res.getURL();
}
return null;
} catch (final Exception ignored) {
return null; // TODO: report?
}
}
/**
* Returns an Enumeration of all the resources in the search path with the specified name.
* <p>
* Gets called from {@link ClassLoader#getResources(String)} after parent class loader was questioned.
*
* @param name The resources to find.
* @return An Enumeration of the resources associated with the name.
* @throws FileSystemException if an error occurs.
*/
@Override
protected Enumeration<URL> findResources(final String name) throws IOException {
final List<URL> result = new ArrayList<>(2);
for (final FileObject baseFile : resources) {
try (FileObject file = baseFile.resolveFile(name, NameScope.DESCENDENT_OR_SELF)) {
if (FileObjectUtils.exists(file)) {
result.add(new Resource(name, baseFile, file).getURL());
}
}
}
return Collections.enumeration(result);
}
/**
* Provide access to the file objects this class loader represents.
*
* @return An array of FileObjects.
* @since 2.0
*/
public FileObject[] getFileObjects() {
return resources.toArray(FileObject.EMPTY_ARRAY);
}
/**
* Calls super.getPermissions both for the code source and also adds the permissions granted to the parent layers.
*
* @param cs the CodeSource.
* @return The PermissionCollections.
*/
@Override
protected PermissionCollection getPermissions(final CodeSource cs) {
try {
final String url = cs.getLocation().toString();
final FileObject file = lookupFileObject(url);
if (file == null) {
return super.getPermissions(cs);
}
final FileObject parentLayer = file.getFileSystem().getParentLayer();
if (parentLayer == null) {
return super.getPermissions(cs);
}
final Permissions combi = new Permissions();
PermissionCollection permCollect = super.getPermissions(cs);
copyPermissions(permCollect, combi);
for (FileObject parent = parentLayer; parent != null; parent = parent.getFileSystem().getParentLayer()) {
final CodeSource parentcs = new CodeSource(parent.getURL(), parent.getContent().getCertificates());
permCollect = super.getPermissions(parentcs);
copyPermissions(permCollect, combi);
}
return combi;
} catch (final FileSystemException fse) {
throw new SecurityException(fse.getMessage());
}
}
/**
* Returns true if we should seal the package where res resides.
*/
private boolean isSealed(final Resource res) throws FileSystemException {
final String sealed = res.getPackageAttribute(Attributes.Name.SEALED);
return "true".equalsIgnoreCase(sealed);
}
/**
* Searches through the search path of for the first class or resource with specified name.
*
* @param name The resource to load.
* @return The Resource.
* @throws FileSystemException if an error occurs.
*/
private Resource loadResource(final String name) throws FileSystemException {
for (final FileObject baseFile : resources) {
try (FileObject file = baseFile.resolveFile(name, NameScope.DESCENDENT_OR_SELF)) {
if (FileObjectUtils.exists(file)) {
return new Resource(name, baseFile, file);
}
}
}
return null;
}
/**
* Does a reverse lookup to find the FileObject when we only have the URL.
*/
private FileObject lookupFileObject(final String name) {
for (final FileObject object : resources) {
if (name.equals(object.getName().getURI())) {
return object;
}
}
return null;
}
}