/*
 * Copyright (C) 2016 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.server.om;

import static com.android.server.om.OverlayManagerService.DEBUG;
import static com.android.server.om.OverlayManagerService.TAG;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.content.om.OverlayConstraint;
import android.content.om.OverlayInfo;
import android.content.om.OverlayableInfo;
import android.os.Build.VERSION_CODES;
import android.os.FabricatedOverlayInfo;
import android.os.FabricatedOverlayInternal;
import android.os.IBinder;
import android.os.IdmapParams;
import android.os.OverlayablePolicy;
import android.os.Parcel;
import android.os.SystemProperties;
import android.text.TextUtils;
import android.util.Slog;

import com.android.server.Watchdog;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.PackageState;

import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;

/**
 * Handle the creation and deletion of idmap files.
 *
 * The actual work is performed by idmap2d.
 * @see IdmapDaemon
 */
final class IdmapManager {
    private static final boolean VENDOR_IS_Q_OR_LATER;
    static {
        final String value = SystemProperties.get("ro.vndk.version", "29");
        boolean isQOrLater;
        try {
            isQOrLater = Integer.parseInt(value) >= 29;
        } catch (NumberFormatException e) {
            // The version is not a number, therefore it is a development codename.
            isQOrLater = true;
        }

        VENDOR_IS_Q_OR_LATER = isQOrLater;
    }

    static final int IDMAP_NOT_EXIST = 0;
    static final int IDMAP_IS_VERIFIED = 1;
    static final int IDMAP_IS_MODIFIED = 1 << 1;

    @IntDef(flag = true, prefix = { "IDMAP_" }, value = {
            IDMAP_NOT_EXIST,
            IDMAP_IS_VERIFIED,
            IDMAP_IS_MODIFIED,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface IdmapStatus {}

    private final IdmapDaemon mIdmapDaemon;
    private final PackageManagerHelper mPackageManager;

    /**
     * Package name of the reference package defined in 'overlay-config-signature' tag of
     * SystemConfig or empty String if tag not defined. This package is vetted on scan by
     * PackageManagerService that it's a system package and is used to check if overlay matches
     * its signature in order to fulfill the config_signature policy.
     */
    private final String mConfigSignaturePackage;

    IdmapManager(final IdmapDaemon idmapDaemon, final PackageManagerHelper packageManager) {
        mPackageManager = packageManager;
        mIdmapDaemon = idmapDaemon;
        mConfigSignaturePackage = packageManager.getConfigSignaturePackage();
    }

    /**
     * Creates the idmap for the target/overlay combination and returns whether the idmap file was
     * modified.
     * @return the status of the specific idmap file. It's one of the following.<ul>
     *     <li>{@link #IDMAP_NOT_EXIST} means the idmap file is not existed.</li>
     *     <li>{@link #IDMAP_IS_VERIFIED} means the idmap file is verified by Idmap2d.</li>
     *     <li>{@link #IDMAP_IS_MODIFIED | IDMAP_IS_VERIFIED } means the idmap file is modified and
     *     verified by Idmap2d.</li>
     * </ul>.
     */
    @IdmapStatus int createIdmap(@NonNull final AndroidPackage targetPackage,
            @NonNull PackageState overlayPackageState, @NonNull final AndroidPackage overlayPackage,
            String overlayBasePath, String overlayName, @UserIdInt int userId,
            @NonNull final List<OverlayConstraint> constraints) {
        if (DEBUG) {
            Slog.d(TAG, "create idmap for " + targetPackage.getPackageName() + " and "
                    + overlayPackage.getPackageName());
        }
        final String targetPath = targetPackage.getSplits().get(0).getPath();
        try {
            int policies = calculateFulfilledPolicies(targetPackage, overlayPackageState,
                    overlayPackage, userId);
            boolean enforce = enforceOverlayable(overlayPackageState, overlayPackage);
            android.os.OverlayConstraint[] idmapConstraints = toIdmapConstraints(constraints);
            if (mIdmapDaemon.verifyIdmap(targetPath, overlayBasePath, overlayName, policies,
                    enforce, userId, idmapConstraints)) {
                return IDMAP_IS_VERIFIED;
            }
            final boolean idmapCreated = mIdmapDaemon.createIdmap(targetPath, overlayBasePath,
                    overlayName, policies, enforce, userId, idmapConstraints) != null;
            return (idmapCreated) ? IDMAP_IS_MODIFIED | IDMAP_IS_VERIFIED : IDMAP_NOT_EXIST;
        } catch (Exception e) {
            Slog.w(TAG, "failed to generate idmap for " + targetPath + " and "
                    + overlayBasePath, e);
            return IDMAP_NOT_EXIST;
        }
    }

    @IdmapStatus int[] createIdmaps(final List<IdmapParams> idmapParams) {
        List<List<IdmapParams>> splitParams = splitIdmapParams(idmapParams);

        int[] idmapStatus = new int[idmapParams.size()];
        try {
            Watchdog.getInstance().pauseWatchingCurrentThread("idmap creation may be slow");
            int startIndex = 0;
            for (List<IdmapParams> params : splitParams) {
                String[] idmapPaths = mIdmapDaemon.verifyOrCreateIdmaps(
                        params.toArray(new IdmapParams[0]));
                // If the idmap2d service isn't ready, idmapPaths will be null and idmapStatus for
                // the corresponding indices will be IDMAP_NOT_EXIST.
                if (idmapPaths != null) {
                    for (int i = 0; i < idmapPaths.length; i++) {
                        // An empty String indicates a verified pre-existing idmap. Otherwise, there
                        // should be a non-empty path or "INVALID" (see Idmap2Service.cpp).
                        idmapStatus[startIndex + i] = idmapPaths[i].isEmpty()
                                ? IDMAP_IS_VERIFIED
                                : idmapPaths[i].equals("INVALID")
                                    ? IDMAP_NOT_EXIST
                                    : IDMAP_IS_MODIFIED | IDMAP_IS_VERIFIED;
                    }
                }
                startIndex += params.size();
            }
        } catch (Exception e) {
            Slog.w(TAG, "failed to generate idmaps", e);
        } finally {
            Watchdog.getInstance().resumeWatchingCurrentThread("idmap creation may be slow");
        }
        return idmapStatus;
    }

    private static final int MAX_IPC_SIZE = IBinder.getSuggestedMaxIpcSizeBytes();

    // Split the input list of IdmapParams so we don't exceed the max IPC size.
    private static List<List<IdmapParams>> splitIdmapParams(final List<IdmapParams> idmapParams) {
        Slog.d(TAG, "splitting " + idmapParams.size()
                + " IdmapParams using max size of " + MAX_IPC_SIZE);
        List<List<IdmapParams>> split = new ArrayList<>();
        final int N = idmapParams.size();

        int startIndex = 0;
        int i = 0;

        Parcel p = Parcel.obtain();
        while (i < N) {
            p.writeInt(1);
            p.writeTypedObject(idmapParams.get(i), 0);
            if (p.dataSize() > MAX_IPC_SIZE) {
                // Don't write the item that caused us to pass the size threshold.
                split.add(idmapParams.subList(startIndex, i));
                Slog.d(TAG, split.size() + ": " + (i - startIndex) + " IdmapParams");
                startIndex = i;
                p.recycle();
                p = Parcel.obtain();
            }
            i++;
        }
        if (startIndex != i) {
            Slog.d(TAG, split.size() + ": " + (i - startIndex) + " IdmapParams");
            split.add(idmapParams.subList(startIndex, i));
        }
        p.recycle();

        return split;
    }

    boolean removeIdmap(@NonNull final OverlayInfo oi, final int userId) {
        if (DEBUG) {
            Slog.d(TAG, "remove idmap for " + oi.baseCodePath);
        }
        try {
            return mIdmapDaemon.removeIdmap(oi.baseCodePath, userId);
        } catch (Exception e) {
            Slog.w(TAG, "failed to remove idmap for " + oi.baseCodePath, e);
            return false;
        }
    }

    boolean idmapExists(@NonNull final OverlayInfo oi) {
        return mIdmapDaemon.idmapExists(oi.baseCodePath, oi.userId);
    }

    /**
     * @return the list of all fabricated overlays
     */
    List<FabricatedOverlayInfo> getFabricatedOverlayInfos() {
        return mIdmapDaemon.getFabricatedOverlayInfos();
    }

    /**
     * Creates a fabricated overlay and persists it to disk.
     * @return the path to the fabricated overlay
     */
    FabricatedOverlayInfo createFabricatedOverlay(@NonNull FabricatedOverlayInternal overlay) {
        return mIdmapDaemon.createFabricatedOverlay(overlay);
    }

    /**
     * Deletes the fabricated overlay file on disk.
     * @return whether the path was deleted
     */
    boolean deleteFabricatedOverlay(@NonNull String path) {
        return mIdmapDaemon.deleteFabricatedOverlay(path);
    }

    /**
     * Gets the idmap data associated with an overlay, in dump format.
     * Only indented for debugging.
     */
    String dumpIdmap(@NonNull String overlayPath) {
        return mIdmapDaemon.dumpIdmap(overlayPath);
    }

    /**
     * Checks if overlayable and policies should be enforced on the specified overlay for backwards
     * compatibility with pre-Q overlays.
     */
    boolean enforceOverlayable(@NonNull PackageState overlayPackageState,
            @NonNull final AndroidPackage overlayPackage) {
        if (overlayPackage.getTargetSdkVersion() >= VERSION_CODES.Q) {
            // Always enforce policies for overlays targeting Q+.
            return true;
        }

        if (overlayPackageState.isVendor()) {
            // If the overlay is on a pre-Q vendor partition, do not enforce overlayable
            // restrictions on this overlay because the pre-Q platform has no understanding of
            // overlayable.
            return VENDOR_IS_Q_OR_LATER;
        }

        // Do not enforce overlayable restrictions on pre-Q overlays that are signed with the
        // platform signature or that are preinstalled.
        return !(overlayPackageState.isSystem() || overlayPackage.isSignedWithPlatformKey());
    }

    /**
     * Retrieves a bitmask for idmap2 that represents the policies the overlay fulfills.
     */
    int calculateFulfilledPolicies(@NonNull final AndroidPackage targetPackage,
            @NonNull PackageState overlayPackageState, @NonNull final AndroidPackage overlayPackage,
            @UserIdInt int userId)  {
        int fulfilledPolicies = OverlayablePolicy.PUBLIC;

        // Overlay matches target signature
        if (mPackageManager.signaturesMatching(targetPackage.getPackageName(),
                overlayPackage.getPackageName(), userId)) {
            fulfilledPolicies |= OverlayablePolicy.SIGNATURE;
        }

        // Overlay matches actor signature
        if (matchesActorSignature(targetPackage, overlayPackage, userId)) {
            fulfilledPolicies |= OverlayablePolicy.ACTOR_SIGNATURE;
        }

        // If SystemConfig defines 'overlay-config-signature' package, given that
        // this package is vetted by OverlayManagerService that it's a
        // preinstalled package, check if overlay matches its signature.
        if (!TextUtils.isEmpty(mConfigSignaturePackage)
                && mPackageManager.signaturesMatching(mConfigSignaturePackage,
                                                           overlayPackage.getPackageName(),
                                                           userId)) {
            fulfilledPolicies |= OverlayablePolicy.CONFIG_SIGNATURE;
        }

        // Vendor partition (/vendor)
        if (overlayPackageState.isVendor()) {
            return fulfilledPolicies | OverlayablePolicy.VENDOR_PARTITION;
        }

        // Product partition (/product)
        if (overlayPackageState.isProduct()) {
            return fulfilledPolicies | OverlayablePolicy.PRODUCT_PARTITION;
        }

        // Odm partition (/odm)
        if (overlayPackageState.isOdm()) {
            return fulfilledPolicies | OverlayablePolicy.ODM_PARTITION;
        }

        // Oem partition (/oem)
        if (overlayPackageState.isOem()) {
            return fulfilledPolicies | OverlayablePolicy.OEM_PARTITION;
        }

        // System_ext partition (/system_ext) is considered as system
        // Check this last since every partition except for data is scanned as system in the PMS.
        if (overlayPackageState.isSystem() || overlayPackageState.isSystemExt()) {
            return fulfilledPolicies | OverlayablePolicy.SYSTEM_PARTITION;
        }

        return fulfilledPolicies;
    }

    private boolean matchesActorSignature(@NonNull AndroidPackage targetPackage,
            @NonNull AndroidPackage overlayPackage, int userId) {
        String targetOverlayableName = overlayPackage.getOverlayTargetOverlayableName();
        if (targetOverlayableName != null && !mPackageManager.getNamedActors().isEmpty()) {
            try {
                OverlayableInfo overlayableInfo = mPackageManager.getOverlayableForTarget(
                        targetPackage.getPackageName(), targetOverlayableName, userId);
                if (overlayableInfo != null && overlayableInfo.actor != null) {
                    String actorPackageName = OverlayActorEnforcer.getPackageNameForActor(
                            overlayableInfo.actor, mPackageManager.getNamedActors()).first;
                    if (mPackageManager.signaturesMatching(actorPackageName,
                            overlayPackage.getPackageName(), userId)) {
                        return true;
                    }
                }
            } catch (IOException ignored) {
            }
        }

        return false;
    }

    @NonNull
    static android.os.OverlayConstraint[] toIdmapConstraints(
            @NonNull final List<OverlayConstraint> constraints) {
        android.os.OverlayConstraint[] idmapConstraints =
                new android.os.OverlayConstraint[constraints.size()];
        int index = 0;
        for (OverlayConstraint constraint : constraints) {
            android.os.OverlayConstraint idmapConstraint = new android.os.OverlayConstraint();
            idmapConstraint.type = constraint.getType();
            idmapConstraint.value = constraint.getValue();
            idmapConstraints[index++] = idmapConstraint;
        }
        return idmapConstraints;
    }
}
