/*
 * Copyright (C) 2014 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 android.os;

import static java.nio.charset.StandardCharsets.UTF_8;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.ArrayMap;
import android.util.Slog;
import android.util.Xml;
import android.util.proto.ProtoOutputStream;

import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;

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

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.ArrayList;

/**
 * A mapping from String keys to values of various types. The set of types
 * supported by this class is purposefully restricted to simple objects that can
 * safely be persisted to and restored from disk.
 *
 * <p><b>Warning:</b> Note that {@link PersistableBundle} is a lazy container and as such it does
 * NOT implement {@link #equals(Object)} or {@link #hashCode()}.
 *
 * @see Bundle
 */
@android.ravenwood.annotation.RavenwoodKeepWholeClass
public final class PersistableBundle extends BaseBundle implements Cloneable, Parcelable,
        XmlUtils.WriteMapCallback {
    private static final String TAG = "PersistableBundle";

    private static final String TAG_PERSISTABLEMAP = "pbundle_as_map";

    /** An unmodifiable {@code PersistableBundle} that is always {@link #isEmpty() empty}. */
    public static final PersistableBundle EMPTY;

    static {
        EMPTY = new PersistableBundle();
        EMPTY.mMap = ArrayMap.EMPTY;
    }

    /** @hide */
    public static boolean isValidType(Object value) {
        return (value instanceof Integer) || (value instanceof Long) ||
                (value instanceof Double) || (value instanceof String) ||
                (value instanceof int[]) || (value instanceof long[]) ||
                (value instanceof double[]) || (value instanceof String[]) ||
                (value instanceof PersistableBundle) || (value == null) ||
                (value instanceof Boolean) || (value instanceof boolean[]);
    }

    /**
     * Constructs a new, empty PersistableBundle.
     */
    public PersistableBundle() {
        super();
        mFlags = FLAG_DEFUSABLE;
    }

    /**
     * Constructs a new, empty PersistableBundle sized to hold the given number of
     * elements. The PersistableBundle will grow as needed.
     *
     * @param capacity the initial capacity of the PersistableBundle
     */
    public PersistableBundle(int capacity) {
        super(capacity);
        mFlags = FLAG_DEFUSABLE;
    }

    /**
     * Constructs a PersistableBundle containing a copy of the mappings from the given
     * PersistableBundle.  Does only a shallow copy of the original PersistableBundle -- see
     * {@link #deepCopy()} if that is not what you want.
     *
     * @param b a PersistableBundle to be copied.
     *
     * @see #deepCopy()
     */
    public PersistableBundle(PersistableBundle b) {
        super(b);
        mFlags = b.mFlags;
    }


    /**
     * Constructs a PersistableBundle from a Bundle.  Does only a shallow copy of the Bundle.
     *
     * <p><b>Warning:</b> This method will deserialize every item on the bundle, including custom
     * types such as {@link Parcelable} and {@link Serializable}, so only use this when you trust
     * the source. Specifically don't use this method on app-provided bundles.
     *
     * @param b a Bundle to be copied.
     *
     * @throws IllegalArgumentException if any element of {@code b} cannot be persisted.
     *
     * @hide
     */
    public PersistableBundle(Bundle b) {
        this(b, true);
    }

    private PersistableBundle(Bundle b, boolean throwException) {
        this(b.getItemwiseMap(), throwException);
    }

    /**
     * Constructs a PersistableBundle containing the mappings passed in.
     *
     * @param map a Map containing only those items that can be persisted.
     * @throws IllegalArgumentException if any element of #map cannot be persisted.
     */
    private PersistableBundle(ArrayMap<String, Object> map, boolean throwException) {
        super();
        mFlags = FLAG_DEFUSABLE;

        // First stuff everything in.
        putAll(map);

        // Now verify each item throwing an exception if there is a violation.
        final int N = mMap.size();
        for (int i = N - 1; i >= 0; --i) {
            Object value = mMap.valueAt(i);
            if (value instanceof ArrayMap) {
                // Fix up any Maps by replacing them with PersistableBundles.
                mMap.setValueAt(i,
                        new PersistableBundle((ArrayMap<String, Object>) value, throwException));
            } else if (value instanceof Bundle) {
                mMap.setValueAt(i, new PersistableBundle((Bundle) value, throwException));
            } else if (!isValidType(value)) {
                final String errorMsg = "Bad value in PersistableBundle key="
                        + mMap.keyAt(i) + " value=" + value;
                if (throwException) {
                    throw new IllegalArgumentException(errorMsg);
                } else {
                    Slog.wtfStack(TAG, errorMsg);
                    mMap.removeAt(i);
                }
            }
        }
    }

    /* package */ PersistableBundle(Parcel parcelledData, int length) {
        super(parcelledData, length);
        mFlags = FLAG_DEFUSABLE;
    }

    /**
     * Constructs a {@link PersistableBundle} containing a copy of {@code from}.
     *
     * @param from The bundle to be copied.
     * @param deep Whether is a deep or shallow copy.
     *
     * @hide
     */
    PersistableBundle(PersistableBundle from, boolean deep) {
        super(from, deep);
    }

    /**
     * Make a PersistableBundle for a single key/value pair.
     *
     * @hide
     */
    public static PersistableBundle forPair(String key, String value) {
        PersistableBundle b = new PersistableBundle(1);
        b.putString(key, value);
        return b;
    }

    /**
     * Clones the current PersistableBundle. The internal map is cloned, but the keys and
     * values to which it refers are copied by reference.
     */
    @Override
    public Object clone() {
        return new PersistableBundle(this);
    }

    /**
     * Make a deep copy of the given bundle.  Traverses into inner containers and copies
     * them as well, so they are not shared across bundles.  Will traverse in to
     * {@link Bundle}, {@link PersistableBundle}, {@link ArrayList}, and all types of
     * primitive arrays.  Other types of objects (such as Parcelable or Serializable)
     * are referenced as-is and not copied in any way.
     */
    public PersistableBundle deepCopy() {
        return new PersistableBundle(this, /* deep */ true);
    }

    /**
     * Inserts a PersistableBundle value into the mapping of this Bundle, replacing
     * any existing value for the given key.  Either key or value may be null.
     *
     * @param key a String, or null
     * @param value a Bundle object, or null
     */
    public void putPersistableBundle(@Nullable String key, @Nullable PersistableBundle value) {
        unparcel();
        mMap.put(key, value);
    }

    /**
     * Returns the value associated with the given key, or null if
     * no mapping of the desired type exists for the given key or a null
     * value is explicitly associated with the key.
     *
     * @param key a String, or null
     * @return a Bundle value, or null
     */
    @Nullable
    public PersistableBundle getPersistableBundle(@Nullable String key) {
        unparcel();
        Object o = mMap.get(key);
        if (o == null) {
            return null;
        }
        try {
            return (PersistableBundle) o;
        } catch (ClassCastException e) {
            typeWarning(key, o, "Bundle", e);
            return null;
        }
    }

    public static final @android.annotation.NonNull Parcelable.Creator<PersistableBundle> CREATOR =
            new Parcelable.Creator<PersistableBundle>() {
                @Override
                public PersistableBundle createFromParcel(Parcel in) {
                    return in.readPersistableBundle();
                }

                @Override
                public PersistableBundle[] newArray(int size) {
                    return new PersistableBundle[size];
                }
            };

    /** @hide */
    @Override
    public void writeUnknownObject(Object v, String name, TypedXmlSerializer out)
            throws XmlPullParserException, IOException {
        if (v instanceof PersistableBundle) {
            out.startTag(null, TAG_PERSISTABLEMAP);
            out.attribute(null, "name", name);
            ((PersistableBundle) v).saveToXml(out);
            out.endTag(null, TAG_PERSISTABLEMAP);
        } else {
            throw new XmlPullParserException("Unknown Object o=" + v);
        }
    }

    /** @hide */
    public void saveToXml(XmlSerializer out) throws IOException, XmlPullParserException {
        saveToXml(XmlUtils.makeTyped(out));
    }

    /** @hide */
    public void saveToXml(TypedXmlSerializer out) throws IOException, XmlPullParserException {
        unparcel();
        // Explicitly drop invalid types an attacker may have added before persisting.
        for (int i = mMap.size() - 1; i >= 0; --i) {
            final Object value = mMap.valueAt(i);
            if (!isValidType(value)) {
                Slog.e(TAG, "Dropping bad data before persisting: "
                        + mMap.keyAt(i) + "=" + value);
                mMap.removeAt(i);
            }
        }
        XmlUtils.writeMapXml(mMap, out, this);
    }

    /**
     * Checks whether all keys and values are within the given character limit.
     * Note: Maximum character limit of String that can be saved to XML as part of bundle is 65535.
     * Otherwise IOException is thrown.
     * @param limit length of String keys and values in the PersistableBundle, including nested
     *                    PersistableBundles to check against.
     *
     * @hide
     */
    public boolean isBundleContentsWithinLengthLimit(int limit) {
        unparcel();
        if (mMap == null) {
            return true;
        }
        for (int i = 0; i < mMap.size(); i++) {
            if (mMap.keyAt(i) != null && mMap.keyAt(i).length() > limit) {
                return false;
            }
            final Object value = mMap.valueAt(i);
            if (value instanceof String && ((String) value).length() > limit) {
                return false;
            } else if (value instanceof String[]) {
                String[] stringArray =  (String[]) value;
                for (int j = 0; j < stringArray.length; j++) {
                    if (stringArray[j] != null
                            && stringArray[j].length() > limit) {
                        return false;
                    }
                }
            } else if (value instanceof PersistableBundle
                    && !((PersistableBundle) value).isBundleContentsWithinLengthLimit(limit)) {
                return false;
            }
        }
        return true;
    }

    /** @hide */
    static class MyReadMapCallback implements  XmlUtils.ReadMapCallback {
        @Override
        public Object readThisUnknownObjectXml(TypedXmlPullParser in, String tag)
                throws XmlPullParserException, IOException {
            if (TAG_PERSISTABLEMAP.equals(tag)) {
                return restoreFromXml(in);
            }
            throw new XmlPullParserException("Unknown tag=" + tag);
        }
    }

    /**
     * Report the nature of this Parcelable's contents
     */
    @Override
    public int describeContents() {
        return 0;
    }

    /**
     * Writes the PersistableBundle contents to a Parcel, typically in order for
     * it to be passed through an IBinder connection.
     * @param parcel The parcel to copy this bundle to.
     */
    @Override
    public void writeToParcel(Parcel parcel, int flags) {
        final boolean oldAllowFds = parcel.pushAllowFds(false);
        try {
            writeToParcelInner(parcel, flags);
        } finally {
            parcel.restoreAllowFds(oldAllowFds);
        }
    }

    /** @hide */
    public static PersistableBundle restoreFromXml(XmlPullParser in) throws IOException,
            XmlPullParserException {
        return restoreFromXml(XmlUtils.makeTyped(in));
    }

    /** @hide */
    public static PersistableBundle restoreFromXml(TypedXmlPullParser in) throws IOException,
            XmlPullParserException {
        final int outerDepth = in.getDepth();
        final String startTag = in.getName();
        final String[] tagName = new String[1];
        int event;
        while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
                (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
            if (event == XmlPullParser.START_TAG) {
                // Don't throw an exception when restoring from XML since an attacker could try to
                // input invalid data in the persisted file.
                return new PersistableBundle((ArrayMap<String, Object>)
                        XmlUtils.readThisArrayMapXml(in, startTag, tagName,
                        new MyReadMapCallback()),
                        /* throwException */ false);
            }
        }
        return new PersistableBundle();  // An empty mutable PersistableBundle
    }

    /**
     * Returns a string representation of the {@link PersistableBundle} that may be suitable for
     * debugging. It won't print the internal map if its content hasn't been unparcelled.
     */
    @Override
    public synchronized String toString() {
        if (mParcelledData != null) {
            if (isEmptyParcel()) {
                return "PersistableBundle[EMPTY_PARCEL]";
            } else {
                return "PersistableBundle[mParcelledData.dataSize=" +
                        mParcelledData.dataSize() + "]";
            }
        }
        return "PersistableBundle[" + mMap.toString() + "]";
    }

    /** @hide */
    synchronized public String toShortString() {
        if (mParcelledData != null) {
            if (isEmptyParcel()) {
                return "EMPTY_PARCEL";
            } else {
                return "mParcelledData.dataSize=" + mParcelledData.dataSize();
            }
        }
        return mMap.toString();
    }

    /** @hide */
    public void dumpDebug(ProtoOutputStream proto, long fieldId) {
        final long token = proto.start(fieldId);

        if (mParcelledData != null) {
            if (isEmptyParcel()) {
                proto.write(PersistableBundleProto.PARCELLED_DATA_SIZE, 0);
            } else {
                proto.write(PersistableBundleProto.PARCELLED_DATA_SIZE, mParcelledData.dataSize());
            }
        } else {
            proto.write(PersistableBundleProto.MAP_DATA, mMap.toString());
        }

        proto.end(token);
    }

    /**
     * Writes the content of the {@link PersistableBundle} to a {@link OutputStream}.
     *
     * <p>The content can be read by a {@link #readFromStream}.
     *
     * @see #readFromStream
     */
    public void writeToStream(@NonNull OutputStream outputStream) throws IOException {
        TypedXmlSerializer serializer = Xml.newFastSerializer();
        serializer.setOutput(outputStream, UTF_8.name());
        serializer.startTag(null, "bundle");
        try {
            saveToXml(serializer);
        } catch (XmlPullParserException e) {
            throw new IOException(e);
        }
        serializer.endTag(null, "bundle");
        serializer.flush();
    }

    /**
     * Reads a {@link PersistableBundle} from an {@link InputStream}.
     *
     * <p>The stream must be generated by {@link #writeToStream}.
     *
     * @see #writeToStream
     */
    @NonNull
    public static PersistableBundle readFromStream(@NonNull InputStream inputStream)
            throws IOException {
        try {
            TypedXmlPullParser parser = Xml.newFastPullParser();
            parser.setInput(inputStream, UTF_8.name());
            parser.next();
            return PersistableBundle.restoreFromXml(parser);
        } catch (XmlPullParserException e) {
            throw new IOException(e);
        }
    }
}
