/*
 * Copyright (C) 2006 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.pm.permission;

import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
import static android.content.pm.PermissionInfo.PROTECTION_NORMAL;
import static android.content.pm.PermissionInfo.PROTECTION_SIGNATURE;
import static android.content.pm.PermissionInfo.PROTECTION_SIGNATURE_OR_SYSTEM;

import static com.android.server.pm.Settings.ATTR_NAME;
import static com.android.server.pm.Settings.ATTR_PACKAGE;
import static com.android.server.pm.Settings.TAG_ITEM;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.pm.PackageParser;
import android.content.pm.PackageParser.Permission;
import android.content.pm.PermissionInfo;
import android.content.pm.Signature;
import android.os.UserHandle;
import android.util.Log;
import android.util.Slog;

import com.android.server.pm.DumpState;
import com.android.server.pm.PackageManagerService;
import com.android.server.pm.PackageSettingBase;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

public final class BasePermission {
    static final String TAG = "PackageManager";

    public static final int TYPE_NORMAL = 0;
    public static final int TYPE_BUILTIN = 1;
    public static final int TYPE_DYNAMIC = 2;
    @IntDef(value = {
        TYPE_NORMAL,
        TYPE_BUILTIN,
        TYPE_DYNAMIC,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface PermissionType {}

    @IntDef(value = {
        PROTECTION_DANGEROUS,
        PROTECTION_NORMAL,
        PROTECTION_SIGNATURE,
        PROTECTION_SIGNATURE_OR_SYSTEM,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ProtectionLevel {}

    final String name;

    final @PermissionType int type;

    String sourcePackageName;

    // TODO: Can we get rid of this? Seems we only use some signature info from the setting
    PackageSettingBase sourcePackageSetting;

    int protectionLevel;

    PackageParser.Permission perm;

    PermissionInfo pendingPermissionInfo;

    /** UID that owns the definition of this permission */
    int uid;

    /** Additional GIDs given to apps granted this permission */
    private int[] gids;

    /**
     * Flag indicating that {@link #gids} should be adjusted based on the
     * {@link UserHandle} the granted app is running as.
     */
    private boolean perUser;

    public BasePermission(String _name, String _sourcePackageName, @PermissionType int _type) {
        name = _name;
        sourcePackageName = _sourcePackageName;
        type = _type;
        // Default to most conservative protection level.
        protectionLevel = PermissionInfo.PROTECTION_SIGNATURE;
    }

    @Override
    public String toString() {
        return "BasePermission{" + Integer.toHexString(System.identityHashCode(this)) + " " + name
                + "}";
    }

    public String getName() {
        return name;
    }
    public int getProtectionLevel() {
        return protectionLevel;
    }
    public String getSourcePackageName() {
        return sourcePackageName;
    }
    public PackageSettingBase getSourcePackageSetting() {
        return sourcePackageSetting;
    }
    public Signature[] getSourceSignatures() {
        return sourcePackageSetting.getSignatures();
    }
    public int getType() {
        return type;
    }
    public int getUid() {
        return uid;
    }
    public void setGids(int[] gids, boolean perUser) {
        this.gids = gids;
        this.perUser = perUser;
    }
    public void setPermission(@Nullable Permission perm) {
        this.perm = perm;
    }
    public void setSourcePackageSetting(PackageSettingBase sourcePackageSetting) {
        this.sourcePackageSetting = sourcePackageSetting;
    }

    public int[] computeGids(int userId) {
        if (perUser) {
            final int[] userGids = new int[gids.length];
            for (int i = 0; i < gids.length; i++) {
                userGids[i] = UserHandle.getUid(userId, gids[i]);
            }
            return userGids;
        } else {
            return gids;
        }
    }

    public int calculateFootprint(BasePermission perm) {
        if (uid == perm.uid) {
            return perm.name.length() + perm.perm.info.calculateFootprint();
        }
        return 0;
    }

    public boolean isPermission(Permission perm) {
        return this.perm == perm;
    }

    public boolean isDynamic() {
        return type == TYPE_DYNAMIC;
    }


    public boolean isNormal() {
        return (protectionLevel & PermissionInfo.PROTECTION_MASK_BASE)
                == PermissionInfo.PROTECTION_NORMAL;
    }
    public boolean isRuntime() {
        return (protectionLevel & PermissionInfo.PROTECTION_MASK_BASE)
                == PermissionInfo.PROTECTION_DANGEROUS;
    }
    public boolean isSignature() {
        return (protectionLevel & PermissionInfo.PROTECTION_MASK_BASE) ==
                PermissionInfo.PROTECTION_SIGNATURE;
    }

    public boolean isAppOp() {
        return (protectionLevel & PermissionInfo.PROTECTION_FLAG_APPOP) != 0;
    }
    public boolean isDevelopment() {
        return isSignature()
                && (protectionLevel & PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) != 0;
    }
    public boolean isInstaller() {
        return (protectionLevel & PermissionInfo.PROTECTION_FLAG_INSTALLER) != 0;
    }
    public boolean isInstant() {
        return (protectionLevel & PermissionInfo.PROTECTION_FLAG_INSTANT) != 0;
    }
    public boolean isOEM() {
        return (protectionLevel & PermissionInfo.PROTECTION_FLAG_OEM) != 0;
    }
    public boolean isPre23() {
        return (protectionLevel & PermissionInfo.PROTECTION_FLAG_PRE23) != 0;
    }
    public boolean isPreInstalled() {
        return (protectionLevel & PermissionInfo.PROTECTION_FLAG_PREINSTALLED) != 0;
    }
    public boolean isPrivileged() {
        return (protectionLevel & PermissionInfo.PROTECTION_FLAG_PRIVILEGED) != 0;
    }
    public boolean isRuntimeOnly() {
        return (protectionLevel & PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY) != 0;
    }
    public boolean isSetup() {
        return (protectionLevel & PermissionInfo.PROTECTION_FLAG_SETUP) != 0;
    }
    public boolean isVerifier() {
        return (protectionLevel & PermissionInfo.PROTECTION_FLAG_VERIFIER) != 0;
    }
    public boolean isVendorPrivileged() {
        return (protectionLevel & PermissionInfo.PROTECTION_FLAG_VENDOR_PRIVILEGED) != 0;
    }

    public void transfer(@NonNull String origPackageName, @NonNull String newPackageName) {
        if (!origPackageName.equals(sourcePackageName)) {
            return;
        }
        sourcePackageName = newPackageName;
        sourcePackageSetting = null;
        perm = null;
        if (pendingPermissionInfo != null) {
            pendingPermissionInfo.packageName = newPackageName;
        }
        uid = 0;
        setGids(null, false);
    }

    public boolean addToTree(@ProtectionLevel int protectionLevel,
            @NonNull PermissionInfo info, @NonNull BasePermission tree) {
        final boolean changed =
                (this.protectionLevel != protectionLevel
                    || perm == null
                    || uid != tree.uid
                    || !perm.owner.equals(tree.perm.owner)
                    || !comparePermissionInfos(perm.info, info));
        this.protectionLevel = protectionLevel;
        info = new PermissionInfo(info);
        info.protectionLevel = protectionLevel;
        perm = new PackageParser.Permission(tree.perm.owner, info);
        perm.info.packageName = tree.perm.info.packageName;
        uid = tree.uid;
        return changed;
    }

    public void updateDynamicPermission(Collection<BasePermission> permissionTrees) {
        if (PackageManagerService.DEBUG_SETTINGS) Log.v(TAG, "Dynamic permission: name="
                + getName() + " pkg=" + getSourcePackageName()
                + " info=" + pendingPermissionInfo);
        if (sourcePackageSetting == null && pendingPermissionInfo != null) {
            final BasePermission tree = findPermissionTree(permissionTrees, name);
            if (tree != null && tree.perm != null) {
                sourcePackageSetting = tree.sourcePackageSetting;
                perm = new PackageParser.Permission(tree.perm.owner,
                        new PermissionInfo(pendingPermissionInfo));
                perm.info.packageName = tree.perm.info.packageName;
                perm.info.name = name;
                uid = tree.uid;
            }
        }
    }

    static BasePermission createOrUpdate(@Nullable BasePermission bp, @NonNull Permission p,
            @NonNull PackageParser.Package pkg, Collection<BasePermission> permissionTrees,
            boolean chatty) {
        final PackageSettingBase pkgSetting = (PackageSettingBase) pkg.mExtras;
        // Allow system apps to redefine non-system permissions
        if (bp != null && !Objects.equals(bp.sourcePackageName, p.info.packageName)) {
            final boolean currentOwnerIsSystem = (bp.perm != null
                    && bp.perm.owner.isSystem());
            if (p.owner.isSystem()) {
                if (bp.type == BasePermission.TYPE_BUILTIN && bp.perm == null) {
                    // It's a built-in permission and no owner, take ownership now
                    bp.sourcePackageSetting = pkgSetting;
                    bp.perm = p;
                    bp.uid = pkg.applicationInfo.uid;
                    bp.sourcePackageName = p.info.packageName;
                    p.info.flags |= PermissionInfo.FLAG_INSTALLED;
                } else if (!currentOwnerIsSystem) {
                    String msg = "New decl " + p.owner + " of permission  "
                            + p.info.name + " is system; overriding " + bp.sourcePackageName;
                    PackageManagerService.reportSettingsProblem(Log.WARN, msg);
                    bp = null;
                }
            }
        }
        if (bp == null) {
            bp = new BasePermission(p.info.name, p.info.packageName, TYPE_NORMAL);
        }
        StringBuilder r = null;
        if (bp.perm == null) {
            if (bp.sourcePackageName == null
                    || bp.sourcePackageName.equals(p.info.packageName)) {
                final BasePermission tree = findPermissionTree(permissionTrees, p.info.name);
                if (tree == null
                        || tree.sourcePackageName.equals(p.info.packageName)) {
                    bp.sourcePackageSetting = pkgSetting;
                    bp.perm = p;
                    bp.uid = pkg.applicationInfo.uid;
                    bp.sourcePackageName = p.info.packageName;
                    p.info.flags |= PermissionInfo.FLAG_INSTALLED;
                    if (chatty) {
                        if (r == null) {
                            r = new StringBuilder(256);
                        } else {
                            r.append(' ');
                        }
                        r.append(p.info.name);
                    }
                } else {
                    Slog.w(TAG, "Permission " + p.info.name + " from package "
                            + p.info.packageName + " ignored: base tree "
                            + tree.name + " is from package "
                            + tree.sourcePackageName);
                }
            } else {
                Slog.w(TAG, "Permission " + p.info.name + " from package "
                        + p.info.packageName + " ignored: original from "
                        + bp.sourcePackageName);
            }
        } else if (chatty) {
            if (r == null) {
                r = new StringBuilder(256);
            } else {
                r.append(' ');
            }
            r.append("DUP:");
            r.append(p.info.name);
        }
        if (bp.perm == p) {
            bp.protectionLevel = p.info.protectionLevel;
        }
        if (PackageManagerService.DEBUG_PACKAGE_SCANNING && r != null) {
            Log.d(TAG, "  Permissions: " + r);
        }
        return bp;
    }

    static BasePermission enforcePermissionTree(
            Collection<BasePermission> permissionTrees, String permName, int callingUid) {
        if (permName != null) {
            BasePermission bp = findPermissionTree(permissionTrees, permName);
            if (bp != null) {
                if (bp.uid == UserHandle.getAppId(callingUid)) {
                    return bp;
                }
                throw new SecurityException("Calling uid " + callingUid
                        + " is not allowed to add to permission tree "
                        + bp.name + " owned by uid " + bp.uid);
            }
        }
        throw new SecurityException("No permission tree found for " + permName);
    }

    public void enforceDeclaredUsedAndRuntimeOrDevelopment(PackageParser.Package pkg) {
        int index = pkg.requestedPermissions.indexOf(name);
        if (index == -1) {
            throw new SecurityException("Package " + pkg.packageName
                    + " has not requested permission " + name);
        }
        if (!isRuntime() && !isDevelopment()) {
            throw new SecurityException("Permission " + name
                    + " is not a changeable permission type");
        }
    }

    private static BasePermission findPermissionTree(
            Collection<BasePermission> permissionTrees, String permName) {
        for (BasePermission bp : permissionTrees) {
            if (permName.startsWith(bp.name) &&
                    permName.length() > bp.name.length() &&
                    permName.charAt(bp.name.length()) == '.') {
                return bp;
            }
        }
        return null;
    }

    public @Nullable PermissionInfo generatePermissionInfo(@NonNull String groupName, int flags) {
        if (groupName == null) {
            if (perm == null || perm.info.group == null) {
                return generatePermissionInfo(protectionLevel, flags);
            }
        } else {
            if (perm != null && groupName.equals(perm.info.group)) {
                return PackageParser.generatePermissionInfo(perm, flags);
            }
        }
        return null;
    }

    public @NonNull PermissionInfo generatePermissionInfo(int adjustedProtectionLevel, int flags) {
        final boolean protectionLevelChanged = protectionLevel != adjustedProtectionLevel;
        // if we return different protection level, don't use the cached info
        if (perm != null && !protectionLevelChanged) {
            return PackageParser.generatePermissionInfo(perm, flags);
        }
        final PermissionInfo pi = new PermissionInfo();
        pi.name = name;
        pi.packageName = sourcePackageName;
        pi.nonLocalizedLabel = name;
        pi.protectionLevel = protectionLevelChanged ? adjustedProtectionLevel : protectionLevel;
        return pi;
    }

    public static boolean readLPw(@NonNull Map<String, BasePermission> out,
            @NonNull XmlPullParser parser) {
        final String tagName = parser.getName();
        if (!tagName.equals(TAG_ITEM)) {
            return false;
        }
        final String name = parser.getAttributeValue(null, ATTR_NAME);
        final String sourcePackage = parser.getAttributeValue(null, ATTR_PACKAGE);
        final String ptype = parser.getAttributeValue(null, "type");
        if (name == null || sourcePackage == null) {
            PackageManagerService.reportSettingsProblem(Log.WARN,
                    "Error in package manager settings: permissions has" + " no name at "
                            + parser.getPositionDescription());
            return false;
        }
        final boolean dynamic = "dynamic".equals(ptype);
        BasePermission bp = out.get(name);
        // If the permission is builtin, do not clobber it.
        if (bp == null || bp.type != TYPE_BUILTIN) {
            bp = new BasePermission(name.intern(), sourcePackage,
                    dynamic ? TYPE_DYNAMIC : TYPE_NORMAL);
        }
        bp.protectionLevel = readInt(parser, null, "protection",
                PermissionInfo.PROTECTION_NORMAL);
        bp.protectionLevel = PermissionInfo.fixProtectionLevel(bp.protectionLevel);
        if (dynamic) {
            final PermissionInfo pi = new PermissionInfo();
            pi.packageName = sourcePackage.intern();
            pi.name = name.intern();
            pi.icon = readInt(parser, null, "icon", 0);
            pi.nonLocalizedLabel = parser.getAttributeValue(null, "label");
            pi.protectionLevel = bp.protectionLevel;
            bp.pendingPermissionInfo = pi;
        }
        out.put(bp.name, bp);
        return true;
    }

    private static int readInt(XmlPullParser parser, String ns, String name, int defValue) {
        String v = parser.getAttributeValue(ns, name);
        try {
            if (v == null) {
                return defValue;
            }
            return Integer.parseInt(v);
        } catch (NumberFormatException e) {
            PackageManagerService.reportSettingsProblem(Log.WARN,
                    "Error in package manager settings: attribute " + name
                            + " has bad integer value " + v + " at "
                            + parser.getPositionDescription());
        }
        return defValue;
    }

    public void writeLPr(@NonNull XmlSerializer serializer) throws IOException {
        if (sourcePackageName == null) {
            return;
        }
        serializer.startTag(null, TAG_ITEM);
        serializer.attribute(null, ATTR_NAME, name);
        serializer.attribute(null, ATTR_PACKAGE, sourcePackageName);
        if (protectionLevel != PermissionInfo.PROTECTION_NORMAL) {
            serializer.attribute(null, "protection", Integer.toString(protectionLevel));
        }
        if (type == BasePermission.TYPE_DYNAMIC) {
            final PermissionInfo pi = perm != null ? perm.info : pendingPermissionInfo;
            if (pi != null) {
                serializer.attribute(null, "type", "dynamic");
                if (pi.icon != 0) {
                    serializer.attribute(null, "icon", Integer.toString(pi.icon));
                }
                if (pi.nonLocalizedLabel != null) {
                    serializer.attribute(null, "label", pi.nonLocalizedLabel.toString());
                }
            }
        }
        serializer.endTag(null, TAG_ITEM);
    }

    private static boolean compareStrings(CharSequence s1, CharSequence s2) {
        if (s1 == null) {
            return s2 == null;
        }
        if (s2 == null) {
            return false;
        }
        if (s1.getClass() != s2.getClass()) {
            return false;
        }
        return s1.equals(s2);
    }

    private static boolean comparePermissionInfos(PermissionInfo pi1, PermissionInfo pi2) {
        if (pi1.icon != pi2.icon) return false;
        if (pi1.logo != pi2.logo) return false;
        if (pi1.protectionLevel != pi2.protectionLevel) return false;
        if (!compareStrings(pi1.name, pi2.name)) return false;
        if (!compareStrings(pi1.nonLocalizedLabel, pi2.nonLocalizedLabel)) return false;
        // We'll take care of setting this one.
        if (!compareStrings(pi1.packageName, pi2.packageName)) return false;
        // These are not currently stored in settings.
        //if (!compareStrings(pi1.group, pi2.group)) return false;
        //if (!compareStrings(pi1.nonLocalizedDescription, pi2.nonLocalizedDescription)) return false;
        //if (pi1.labelRes != pi2.labelRes) return false;
        //if (pi1.descriptionRes != pi2.descriptionRes) return false;
        return true;
    }

    public boolean dumpPermissionsLPr(@NonNull PrintWriter pw, @NonNull String packageName,
            @NonNull Set<String> permissionNames, boolean readEnforced,
            boolean printedSomething, @NonNull DumpState dumpState) {
        if (packageName != null && !packageName.equals(sourcePackageName)) {
            return false;
        }
        if (permissionNames != null && !permissionNames.contains(name)) {
            return false;
        }
        if (!printedSomething) {
            if (dumpState.onTitlePrinted())
                pw.println();
            pw.println("Permissions:");
            printedSomething = true;
        }
        pw.print("  Permission ["); pw.print(name); pw.print("] (");
                pw.print(Integer.toHexString(System.identityHashCode(this)));
                pw.println("):");
        pw.print("    sourcePackage="); pw.println(sourcePackageName);
        pw.print("    uid="); pw.print(uid);
                pw.print(" gids="); pw.print(Arrays.toString(
                        computeGids(UserHandle.USER_SYSTEM)));
                pw.print(" type="); pw.print(type);
                pw.print(" prot=");
                pw.println(PermissionInfo.protectionToString(protectionLevel));
        if (perm != null) {
            pw.print("    perm="); pw.println(perm);
            if ((perm.info.flags & PermissionInfo.FLAG_INSTALLED) == 0
                    || (perm.info.flags & PermissionInfo.FLAG_REMOVED) != 0) {
                pw.print("    flags=0x"); pw.println(Integer.toHexString(perm.info.flags));
            }
        }
        if (sourcePackageSetting != null) {
            pw.print("    packageSetting="); pw.println(sourcePackageSetting);
        }
        if (READ_EXTERNAL_STORAGE.equals(name)) {
            pw.print("    enforced=");
            pw.println(readEnforced);
        }
        return true;
    }
}
