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