001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.vfs2.impl;
018
019import java.io.IOException;
020import java.net.URL;
021import java.security.CodeSource;
022import java.security.Permission;
023import java.security.PermissionCollection;
024import java.security.Permissions;
025import java.security.SecureClassLoader;
026import java.security.cert.Certificate;
027import java.util.ArrayList;
028import java.util.Collections;
029import java.util.Enumeration;
030import java.util.Iterator;
031import java.util.List;
032import java.util.jar.Attributes;
033import java.util.jar.Attributes.Name;
034
035import org.apache.commons.vfs2.FileObject;
036import org.apache.commons.vfs2.FileSystemException;
037import org.apache.commons.vfs2.FileSystemManager;
038import org.apache.commons.vfs2.NameScope;
039
040/**
041 * A class loader that can load classes and resources from a search path.
042 * <p>
043 * The search path can consist of VFS FileObjects referring both to folders and JAR files. Any FileObject of type
044 * FileType.FILE is assumed to be a JAR and is opened by creating a layered file system with the "jar" scheme.
045 * <p>
046 * TODO - Test this with signed Jars and a SecurityManager.
047 *
048 * @see FileSystemManager#createFileSystem
049 */
050public class VFSClassLoader extends SecureClassLoader {
051    private final ArrayList<FileObject> resources = new ArrayList<>();
052
053    /**
054     * Constructors a new VFSClassLoader for the given file.
055     *
056     * @param file the file to load the classes and resources from.
057     * @param manager the FileManager to use when trying create a layered Jar file system.
058     * @throws FileSystemException if an error occurs.
059     */
060    public VFSClassLoader(final FileObject file, final FileSystemManager manager) throws FileSystemException {
061        this(new FileObject[] { file }, manager, null);
062    }
063
064    /**
065     * Constructors a new VFSClassLoader for the given file.
066     *
067     * @param file the file to load the classes and resources from.
068     * @param manager the FileManager to use when trying create a layered Jar file system.
069     * @param parent the parent class loader for delegation.
070     * @throws FileSystemException if an error occurs.
071     */
072    public VFSClassLoader(final FileObject file, final FileSystemManager manager, final ClassLoader parent)
073            throws FileSystemException {
074        this(new FileObject[] { file }, manager, parent);
075    }
076
077    /**
078     * Constructors a new VFSClassLoader for the given files. The files will be searched in the order specified.
079     *
080     * @param files the files to load the classes and resources from.
081     * @param manager the FileManager to use when trying create a layered Jar file system.
082     * @throws FileSystemException if an error occurs.
083     */
084    public VFSClassLoader(final FileObject[] files, final FileSystemManager manager) throws FileSystemException {
085        this(files, manager, null);
086    }
087
088    /**
089     * Constructors a new VFSClassLoader for the given FileObjects. The FileObjects will be searched in the order
090     * specified.
091     *
092     * @param files the FileObjects to load the classes and resources from.
093     * @param manager the FileManager to use when trying create a layered Jar file system.
094     * @param parent the parent class loader for delegation.
095     * @throws FileSystemException if an error occurs.
096     */
097    public VFSClassLoader(final FileObject[] files, final FileSystemManager manager, final ClassLoader parent)
098            throws FileSystemException {
099        super(parent);
100        addFileObjects(manager, files);
101    }
102
103    /**
104     * Provide access to the file objects this class loader represents.
105     *
106     * @return An array of FileObjects.
107     * @since 2.0
108     */
109    public FileObject[] getFileObjects() {
110        return resources.toArray(new FileObject[resources.size()]);
111    }
112
113    /**
114     * Appends the specified FileObjects to the list of FileObjects to search for classes and resources.
115     *
116     * @param manager The FileSystemManager.
117     * @param files the FileObjects to append to the search path.
118     * @throws FileSystemException if an error occurs.
119     */
120    private void addFileObjects(final FileSystemManager manager, final FileObject[] files) throws FileSystemException {
121        for (FileObject file : files) {
122            if (!file.exists()) {
123                // Does not exist - skip
124                continue;
125            }
126
127            // TODO - use federation instead
128            if (manager.canCreateFileSystem(file)) {
129                // Use contents of the file
130                file = manager.createFileSystem(file);
131            }
132
133            resources.add(file);
134        }
135    }
136
137    /**
138     * Finds and loads the class with the specified name from the search path.
139     *
140     * @throws ClassNotFoundException if the class is not found.
141     */
142    @Override
143    protected Class<?> findClass(final String name) throws ClassNotFoundException {
144        try {
145            final String path = name.replace('.', '/').concat(".class");
146            final Resource res = loadResource(path);
147            if (res == null) {
148                throw new ClassNotFoundException(name);
149            }
150            return defineClass(name, res);
151        } catch (final IOException ioe) {
152            throw new ClassNotFoundException(name, ioe);
153        }
154    }
155
156    /**
157     * Loads and verifies the class with name and located with res.
158     */
159    private Class<?> defineClass(final String name, final Resource res) throws IOException {
160        final URL url = res.getCodeSourceURL();
161        final String pkgName = res.getPackageName();
162        if (pkgName != null) {
163            final Package pkg = getPackage(pkgName);
164            if (pkg != null) {
165                if (pkg.isSealed()) {
166                    if (!pkg.isSealed(url)) {
167                        throw new FileSystemException("vfs.impl/pkg-sealed-other-url", pkgName);
168                    }
169                } else {
170                    if (isSealed(res)) {
171                        throw new FileSystemException("vfs.impl/pkg-sealing-unsealed", pkgName);
172                    }
173                }
174            } else {
175                definePackage(pkgName, res);
176            }
177        }
178
179        final byte[] bytes = res.getBytes();
180        final Certificate[] certs = res.getFileObject().getContent().getCertificates();
181        final CodeSource cs = new CodeSource(url, certs);
182        return defineClass(name, bytes, 0, bytes.length, cs);
183    }
184
185    /**
186     * Returns true if the we should seal the package where res resides.
187     */
188    private boolean isSealed(final Resource res) throws FileSystemException {
189        final String sealed = res.getPackageAttribute(Attributes.Name.SEALED);
190        return "true".equalsIgnoreCase(sealed);
191    }
192
193    /**
194     * Reads attributes for the package and defines it.
195     */
196    private Package definePackage(final String name, final Resource res) throws FileSystemException {
197        // TODO - check for MANIFEST_ATTRIBUTES capability first
198        final String specTitle = res.getPackageAttribute(Name.SPECIFICATION_TITLE);
199        final String specVendor = res.getPackageAttribute(Attributes.Name.SPECIFICATION_VENDOR);
200        final String specVersion = res.getPackageAttribute(Name.SPECIFICATION_VERSION);
201        final String implTitle = res.getPackageAttribute(Name.IMPLEMENTATION_TITLE);
202        final String implVendor = res.getPackageAttribute(Name.IMPLEMENTATION_VENDOR);
203        final String implVersion = res.getPackageAttribute(Name.IMPLEMENTATION_VERSION);
204
205        final URL sealBase;
206        if (isSealed(res)) {
207            sealBase = res.getCodeSourceURL();
208        } else {
209            sealBase = null;
210        }
211
212        return definePackage(name, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor, sealBase);
213    }
214
215    /**
216     * Calls super.getPermissions both for the code source and also adds the permissions granted to the parent layers.
217     *
218     * @param cs the CodeSource.
219     * @return The PermissionCollections.
220     */
221    @Override
222    protected PermissionCollection getPermissions(final CodeSource cs) {
223        try {
224            final String url = cs.getLocation().toString();
225            final FileObject file = lookupFileObject(url);
226            if (file == null) {
227                return super.getPermissions(cs);
228            }
229
230            final FileObject parentLayer = file.getFileSystem().getParentLayer();
231            if (parentLayer == null) {
232                return super.getPermissions(cs);
233            }
234
235            final Permissions combi = new Permissions();
236            PermissionCollection permCollect = super.getPermissions(cs);
237            copyPermissions(permCollect, combi);
238
239            for (FileObject parent = parentLayer; parent != null; parent = parent.getFileSystem().getParentLayer()) {
240                final CodeSource parentcs = new CodeSource(parent.getURL(), parent.getContent().getCertificates());
241                permCollect = super.getPermissions(parentcs);
242                copyPermissions(permCollect, combi);
243            }
244
245            return combi;
246        } catch (final FileSystemException fse) {
247            throw new SecurityException(fse.getMessage());
248        }
249    }
250
251    /**
252     * Copies the permissions from src to dest.
253     *
254     * @param src The source PermissionCollection.
255     * @param dest The destination PermissionCollection.
256     */
257    protected void copyPermissions(final PermissionCollection src, final PermissionCollection dest) {
258        for (final Enumeration<Permission> elem = src.elements(); elem.hasMoreElements();) {
259            final Permission permission = elem.nextElement();
260            dest.add(permission);
261        }
262    }
263
264    /**
265     * Does a reverse lookup to find the FileObject when we only have the URL.
266     */
267    private FileObject lookupFileObject(final String name) {
268        final Iterator<FileObject> it = resources.iterator();
269        while (it.hasNext()) {
270            final FileObject object = it.next();
271            if (name.equals(object.getName().getURI())) {
272                return object;
273            }
274        }
275        return null;
276    }
277
278    /**
279     * Finds the resource with the specified name from the search path. This returns null if the resource is not found.
280     *
281     * @param name The resource name.
282     * @return The URL that matches the resource.
283     */
284    @Override
285    protected URL findResource(final String name) {
286        try {
287            final Resource res = loadResource(name);
288            if (res != null) {
289                return res.getURL();
290            }
291            return null;
292        } catch (final Exception ignored) {
293            return null; // TODO: report?
294        }
295    }
296
297    /**
298     * Returns an Enumeration of all the resources in the search path with the specified name.
299     * <p>
300     * Gets called from {@link ClassLoader#getResources(String)} after parent class loader was questioned.
301     *
302     * @param name The resources to find.
303     * @return An Enumeration of the resources associated with the name.
304     * @throws FileSystemException if an error occurs.
305     */
306    @Override
307    protected Enumeration<URL> findResources(final String name) throws IOException {
308        final List<URL> result = new ArrayList<>(2);
309
310        for (final FileObject baseFile : resources) {
311            final FileObject file = baseFile.resolveFile(name, NameScope.DESCENDENT_OR_SELF);
312            if (file.exists()) {
313                result.add(new Resource(name, baseFile, file).getURL());
314            }
315        }
316
317        return Collections.enumeration(result);
318    }
319
320    /**
321     * Searches through the search path of for the first class or resource with specified name.
322     *
323     * @param name The resource to load.
324     * @return The Resource.
325     * @throws FileSystemException if an error occurs.
326     */
327    private Resource loadResource(final String name) throws FileSystemException {
328        for (final FileObject baseFile : resources) {
329            final FileObject file = baseFile.resolveFile(name, NameScope.DESCENDENT_OR_SELF);
330            if (file.exists()) {
331                return new Resource(name, baseFile, file);
332            }
333        }
334        return null;
335    }
336}