NativeCodeLoader.java

 /*
 * 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.crypto;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFileAttributes;
import java.util.Objects;
import java.util.Properties;
import java.util.UUID;

import org.apache.commons.crypto.utils.Utils;

/**
 * A helper to load the native code i.e. libcommons-crypto.so. This handles the
 * fallback to either the bundled libcommons-crypto-Linux-i386-32.so or the
 * default java implementations where appropriate.
 */
final class NativeCodeLoader {

    private static final String SIMPLE_NAME = NativeCodeLoader.class.getSimpleName();

    private static final String NATIVE_LIBNAME = "commons-crypto";

    private static final String NATIVE_LIBNAME_ALT = "lib" + NATIVE_LIBNAME + ".jnilib";

    /**
     * End of file pseudo-character.
     */
    private static final int EOF = -1;

    private static final Throwable libraryLoadingError;

    private static final boolean libraryLoaded;

    static {
        debug("%s static init start", SIMPLE_NAME);
        libraryLoadingError = loadLibrary(); // will be null if loaded OK
        libraryLoaded = libraryLoadingError == null;
        debug("%s libraryLoaded = %s, libraryLoadingError = %s", SIMPLE_NAME, libraryLoaded, libraryLoadingError);
        debug("%s static init end", SIMPLE_NAME);
    }

    /**
     * Returns the given InputStream if it is already a {@link BufferedInputStream},
     * otherwise creates a BufferedInputStream from the given InputStream.
     * <p>
     * Copied from Apache Commons IO 2.5.
     * </p>
     *
     * @param inputStream the InputStream to wrap or return (not null)
     * @return the given InputStream or a new {@link BufferedInputStream} for the
     *         given InputStream
     * @throws NullPointerException if the input parameter is null
     */
    @SuppressWarnings("resource")
    private static BufferedInputStream buffer(final InputStream inputStream) {
        // reject null early on rather than waiting for IO operation to fail
        // not checked by BufferedInputStream
        Objects.requireNonNull(inputStream, "inputStream");
        return inputStream instanceof BufferedInputStream ? (BufferedInputStream) inputStream
                : new BufferedInputStream(inputStream);
    }

    /**
     * Checks whether in1 and in2 is equal.
     * <p>
     * Copied from Apache Commons IO 2.5.
     * </p>
     *
     * @param input1 the input1.
     * @param input2 the input2.
     * @return true if in1 and in2 is equal, else false.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("resource")
    private static boolean contentsEquals(final InputStream input1, final InputStream input2) throws IOException {
        if (input1 == input2) {
            return true;
        }
        if (input1 == null ^ input2 == null) {
            return false;
        }
        final BufferedInputStream bufferedInput1 = buffer(input1);
        final BufferedInputStream bufferedInput2 = buffer(input2);
        int ch = bufferedInput1.read();
        while (EOF != ch) {
            final int ch2 = bufferedInput2.read();
            if (ch != ch2) {
                return false;
            }
            ch = bufferedInput1.read();
        }
        return bufferedInput2.read() == EOF;
    }

    /**
     * Logs debug messages.
     *
     * @param format See {@link String#format(String, Object...)}.
     * @param args   See {@link String#format(String, Object...)}.
     */
    private static void debug(final String format, final Object... args) {
        // TODO Find a better way to do this later.
        if (isDebug()) {
            System.out.println(String.format(format, args));
            if (args != null && args.length > 0 && args[0] instanceof Throwable) {
                ((Throwable) args[0]).printStackTrace(System.out);
            }
        }
    }

    /**
     * Extracts the specified library file to the target folder.
     *
     * @param libFolderForCurrentOS the library in commons-crypto.lib.path.
     * @param libraryFileName       the library name.
     * @param targetFolder          Target folder for the native lib. Use the value
     *                              of commons-crypto.tempdir or java.io.tmpdir.
     * @return the library file.
     */
    private static File extractLibraryFile(final String libFolderForCurrentOS, final String libraryFileName,
            final String targetFolder) {
        final String nativeLibraryFilePath = libFolderForCurrentOS + File.separator + libraryFileName;

        // Attach UUID to the native library file to ensure multiple class loaders
        // can read the libcommons-crypto multiple times.
        final UUID uuid = UUID.randomUUID();
        final String extractedLibFileName = String.format("commons-crypto-%s-%s", uuid, libraryFileName);
        final File extractedLibFile = new File(targetFolder, extractedLibFileName);
        debug("Extracting '%s' to '%s'...", nativeLibraryFilePath, extractedLibFile);
        try (InputStream inputStream = NativeCodeLoader.class.getResourceAsStream(nativeLibraryFilePath)) {
            if (inputStream == null) {
                debug("Resource not found: %s", nativeLibraryFilePath);
                return null;
            }
            // Extract a native library file into the target directory
            final Path path;
            try {
                path = extractedLibFile.toPath();
                final long byteCount = Files.copy(inputStream, path, StandardCopyOption.REPLACE_EXISTING);
                if (isDebug()) {
                    debug("Extracted '%s' to '%s': %,d bytes [%s]", nativeLibraryFilePath, extractedLibFile, byteCount,
                            Files.isExecutable(path) ? "X+" : "X-");
                    final PosixFileAttributes attributes = Files.readAttributes(path, PosixFileAttributes.class);
                    if (attributes != null) {
                        debug("Attributes '%s': %s %s %s", extractedLibFile, attributes.permissions(),
                                attributes.owner(), attributes.group());
                    }
                }
            } finally {
                // Delete the extracted lib file on JVM exit.
                debug("Delete on exit: %s", extractedLibFile);
                extractedLibFile.deleteOnExit();
            }

            // Set executable (x) flag to enable Java to load the native library
            if (!extractedLibFile.setReadable(true) || !extractedLibFile.setExecutable(true)
                    || !extractedLibFile.setWritable(true, true)) {
                throw new IllegalStateException("Invalid path for library path " + extractedLibFile);
            }

            // Check whether the contents are properly copied from the resource
            // folder
            try (InputStream nativeInputStream = NativeCodeLoader.class.getResourceAsStream(nativeLibraryFilePath)) {
                try (InputStream extractedLibIn = Files.newInputStream(path)) {
                    debug("Validating '%s'...", extractedLibFile);
                    if (!contentsEquals(nativeInputStream, extractedLibIn)) {
                        throw new IllegalStateException(String.format("Failed to write a native library file %s to %s",
                                nativeLibraryFilePath, extractedLibFile));
                    }
                }
            }
            return extractedLibFile;
        } catch (final IOException e) {
            debug("Ignoring %s", e);
            return null;
        }
    }

    /**
     * Finds the native library.
     *
     * @return the jar file.
     */
    private static File findNativeLibrary() {
        // Get the properties once
        final Properties props = Utils.getDefaultProperties();

        // Try to load the library in commons-crypto.lib.path */
        String nativeLibraryPath = props.getProperty(Crypto.LIB_PATH_KEY);
        String nativeLibraryName = props.getProperty(Crypto.LIB_NAME_KEY, System.mapLibraryName(NATIVE_LIBNAME));

        debug("%s nativeLibraryPath %s = %s", SIMPLE_NAME, Crypto.LIB_PATH_KEY, nativeLibraryPath);
        debug("%s nativeLibraryName %s = %s", SIMPLE_NAME, Crypto.LIB_NAME_KEY, nativeLibraryName);

        if (nativeLibraryPath != null) {
            final File nativeLib = new File(nativeLibraryPath, nativeLibraryName);
            final boolean exists = nativeLib.exists();
            debug("%s nativeLib %s exists = %s", SIMPLE_NAME, nativeLib, exists);
            if (exists) {
                return nativeLib;
            }
        }

        // Load an OS-dependent native library inside a jar file
        nativeLibraryPath = "/org/apache/commons/crypto/native/" + OsInfo.getNativeLibFolderPathForCurrentOS();
        debug("%s nativeLibraryPath = %s", SIMPLE_NAME, nativeLibraryPath);
        final String resource = nativeLibraryPath + File.separator + nativeLibraryName;
        boolean hasNativeLib = hasResource(resource);
        debug("%s resource %s exists = %s", SIMPLE_NAME, resource, hasNativeLib);
        if (!hasNativeLib) {
            final String altName = NATIVE_LIBNAME_ALT;
            if (OsInfo.getOSName().equals("Mac") && hasResource(nativeLibraryPath + File.separator + altName)) {
                // Fix for openjdk7 for Mac
                nativeLibraryName = altName;
                hasNativeLib = true;
            }
        }

        if (!hasNativeLib) {
            final String errorMessage = String.format("No native library is found for os.name=%s and os.arch=%s", OsInfo.getOSName(), OsInfo.getArchName());
            throw new IllegalStateException(errorMessage);
        }

        // Temporary folder for the native lib. Use the value of
        // Crypto.LIB_TEMPDIR_KEY or java.io.tmpdir
        final String tempFolder = new File(props.getProperty(Crypto.LIB_TEMPDIR_KEY, System.getProperty("java.io.tmpdir"))).getAbsolutePath();

        // Extract and load a native library inside the jar file
        return extractLibraryFile(nativeLibraryPath, nativeLibraryName, tempFolder);
    }

    /**
     * Gets the error cause if loading failed.
     *
     * @return null, unless loading failed
     */
    static Throwable getLoadingError() {
        return libraryLoadingError;
    }

    /**
     * Checks whether the given path has resource.
     *
     * @param path the path.
     * @return the boolean.
     */
    private static boolean hasResource(final String path) {
        return NativeCodeLoader.class.getResource(path) != null;
    }

    private static boolean isDebug() {
        return Boolean.getBoolean(Crypto.CONF_PREFIX + "debug");
    }

    /**
     * Checks whether native code is loaded for this platform.
     *
     * @return {@code true} if native is loaded, else {@code false}.
     */
    static boolean isNativeCodeLoaded() {
        return libraryLoaded;
    }

    /**
     * Loads the library if possible.
     *
     * @return null if successful, otherwise the Throwable that was caught
     */
    static Throwable loadLibrary() {
        try {
            final File nativeLibFile = findNativeLibrary();
            if (nativeLibFile != null) {
                // Load extracted or specified native library.
                final String absolutePath = nativeLibFile.getAbsolutePath();
                debug("%s System.load('%s')", SIMPLE_NAME, absolutePath);
                System.load(absolutePath);
            } else {
                // Load preinstalled library (in the path -Djava.library.path)
                final String libname = NATIVE_LIBNAME;
                debug("%s System.loadLibrary('%s')", SIMPLE_NAME, libname);
                System.loadLibrary(libname);
            }
            return null; // OK
        } catch (final Exception | UnsatisfiedLinkError t) {
            return t;
        }
    }

    /**
     * The private constructor of {@link NativeCodeLoader}.
     */
    private NativeCodeLoader() {
    }
}