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

Warning: 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. * *

Warning: 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 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) 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 CREATOR = new Parcelable.Creator() { @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) 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}. * *

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}. * *

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); } } }