/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed 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 com.android.tools.lint.checks;

import static com.android.SdkConstants.DOT_NATIVE_LIBS;

import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaEvaluator;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.JavaPsiScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class UnsafeNativeCodeDetector extends Detector implements JavaPsiScanner {

    private static final Implementation IMPLEMENTATION = new Implementation(
            UnsafeNativeCodeDetector.class,
            Scope.JAVA_FILE_SCOPE);

    public static final Issue LOAD = Issue.create(
            "UnsafeDynamicallyLoadedCode",
            "`load` used to dynamically load code",
            "Dynamically loading code from locations other than the application's library " +
            "directory or the Android platform's built-in library directories is dangerous, " +
            "as there is an increased risk that the code could have been tampered with. " +
            "Applications should use `loadLibrary` when possible, which provides increased " +
            "assurance that libraries are loaded from one of these safer locations. " +
            "Application developers should use the features of their development " +
            "environment to place application native libraries into the lib directory " +
            "of their compiled APKs.",
            Category.SECURITY,
            4,
            Severity.WARNING,
            IMPLEMENTATION);

    public static final Issue UNSAFE_NATIVE_CODE_LOCATION = Issue.create(
            "UnsafeNativeCodeLocation", //$NON-NLS-1$
            "Native code outside library directory",
            "In general, application native code should only be placed in the application's " +
            "library directory, not in other locations such as the res or assets directories. " +
            "Placing the code in the library directory provides increased assurance that the " +
            "code will not be tampered with after application installation. Application " +
            "developers should use the features of their development environment to place " +
            "application native libraries into the lib directory of their compiled " +
            "APKs. Embedding non-shared library native executables into applications should " +
            "be avoided when possible.",
            Category.SECURITY,
            4,
            Severity.WARNING,
            IMPLEMENTATION);

    private static final String RUNTIME_CLASS = "java.lang.Runtime"; //$NON-NLS-1$
    private static final String SYSTEM_CLASS = "java.lang.System"; //$NON-NLS-1$

    private static final byte[] ELF_MAGIC_VALUE = { (byte) 0x7F, (byte) 0x45, (byte) 0x4C, (byte) 0x46 };

    @NonNull
    @Override
    public Speed getSpeed() {
        return Speed.NORMAL;
    }

    // ---- Implements Detector.JavaScanner ----

    @Override
    public List<String> getApplicableMethodNames() {
        // Identify calls to Runtime.load() and System.load()
        return Collections.singletonList("load");
    }

    @Override
    public void visitMethod(@NonNull JavaContext context, @Nullable JavaElementVisitor visitor,
            @NonNull PsiMethodCallExpression call, @NonNull PsiMethod method) {
        // Report calls to Runtime.load() and System.load()
        if ("load".equals(method.getName())) {
            JavaEvaluator evaluator = context.getEvaluator();
            if (evaluator.isMemberInSubClassOf(method, RUNTIME_CLASS, false) ||
                    evaluator.isMemberInSubClassOf(method, SYSTEM_CLASS, false)) {
                context.report(LOAD, call, context.getLocation(call),
                        "Dynamically loading code using `load` is risky, please use " +
                                "`loadLibrary` instead when possible");
            }
        }
    }

    // ---- Look for code in resource and asset directories ----

    @Override
    public void afterCheckLibraryProject(@NonNull Context context) {
        if (!context.getProject().getReportIssues()) {
            // If this is a library project not being analyzed, ignore it
            return;
        }

        checkResourceFolders(context, context.getProject());
    }

    @Override
    public void afterCheckProject(@NonNull Context context) {
        if (!context.getProject().getReportIssues()) {
            // If this is a library project not being analyzed, ignore it
            return;
        }

        checkResourceFolders(context, context.getProject());
    }

    private static boolean isNativeCode(File file) {
        if (!file.isFile()) {
            return false;
        }

        try {
            FileInputStream fis = new FileInputStream(file);
            try {
                byte[] bytes = new byte[4];
                int length = fis.read(bytes);
                return (length == 4) && (Arrays.equals(ELF_MAGIC_VALUE, bytes));
            } finally {
                fis.close();
            }
        } catch (IOException ex) {
            return false;
        }
    }

    private static void checkResourceFolders(Context context, @NonNull Project project) {
        if (!context.getScope().contains(Scope.RESOURCE_FOLDER)) {
            // Don't do work when doing in-editor analysis of Java files
            return;
        }
        List<File> resourceFolders = project.getResourceFolders();
        for (File res : resourceFolders) {
            File[] folders = res.listFiles();
            if (folders != null) {
                for (File typeFolder : folders) {
                    if (typeFolder.getName().startsWith(SdkConstants.FD_RES_RAW)) {
                        File[] rawFiles = typeFolder.listFiles();
                        if (rawFiles != null) {
                            for (File rawFile : rawFiles) {
                                checkFile(context, rawFile);
                            }
                        }
                    }
                }
            }
        }

        List<File> assetFolders = project.getAssetFolders();
        for (File assetFolder : assetFolders) {
            File[] assets = assetFolder.listFiles();
            if (assets != null) {
                for (File asset : assets) {
                    checkFile(context, asset);
                }
            }
        }
    }

    private static void checkFile(@NonNull Context context, @NonNull File file) {
        if (isNativeCode(file)) {
            if (LintUtils.endsWith(file.getPath(), DOT_NATIVE_LIBS)) {
                context.report(UNSAFE_NATIVE_CODE_LOCATION, Location.create(file),
                        "Shared libraries should not be placed in the res or assets " +
                        "directories. Please use the features of your development " +
                        "environment to place shared libraries in the lib directory of " +
                        "the compiled APK.");
            } else {
                context.report(UNSAFE_NATIVE_CODE_LOCATION, Location.create(file),
                        "Embedding non-shared library native executables into applications " +
                        "should be avoided when possible, as there is an increased risk that " +
                        "the executables could be tampered with after installation. Instead, " +
                        "native code should be placed in a shared library, and the features of " +
                        "the development environment should be used to place the shared library " +
                        "in the lib directory of the compiled APK.");
            }
        }
    }
}
