Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cccb1b78 authored by Bernardo Rufino's avatar Bernardo Rufino
Browse files

Add safer Bundle APIs and deprecated old ones

Add safer Bundle APIs that take an extra Class<T> argument that checks
that the type about to be deserialized is a child of the type passed in
parameter *before* actually deserializing it, while also deprecating old
APIs.

This allows use to reap the benefits of the new typed Parcel APIs and
enhances security.

Only the APIs that could involve custom object injection are modified.
So, besides the obvious ones that have that design (eg.
readParcelableList()), subtler cases such as readIntegerArrayList()
could result in custom object deserialization, and since it's all
generics, even the casting inside Bundle wouldn't fail, only after the
client unpacked the list items would it blow up. Now those are checked
beforehand.

Since Bundle always calls Parcel.readValue() under the hood (instead of
specialized APIs such as readParcelable() etc), we had to augment that
method (that's used by LazyValue when retrieving the item) to accept
item types now for containers, which I implemented as a vararg of
Class<?> parameters (this is all private/@hide). This way we could
retrieve a list of intents like readValue(.., List.class, Intent.class),
or a map of string to intents like readValue(.., Map.class,
String.class, Intent.class). For non-container items, we can just pass
no arguments for the vararg. This is explained in internal javadocs.

Inside readValue() now, we also check the container types before
calling the internal methods for deserialization. So, if the thing on
the wire is a VAL_MAP and we know the method we're about to call will
return a HashMap, we verify that the type passed in parameter is a super
type of that (if it's non-null, if it's null it means "perform no
check").

Now, LazyValue became a BiFunction<Class<?>, Class<?>[], Object> to
receive those extra "item types" for containers. The reason for
separating the first from the rest is that the first defines the return
type in the new APIs and inside Parcel, so we need the T from Class<T>
to ensure type-safety.

(I was torn here between using BiFunction or just exposing LazyValue as
@hide for Bundle since it feels like we're missing meaning/abstraction,
but end up leaving this way, advise if you'd prefer the other way)

There was a bit of a refactor in Parcel so readValue() could call
internal methods that accepted nullable Class<?> parameters with the
meaning that null = "no verification"  and non-null = "check against
type provided" (because the external APIs all require non-null
parameters).

Now we can return null in all cases when there is a type mismatch. Note
that the Bundle APIs catch ClassCastException to return null, but that
only works for non-generic types (eg. getSizeF()). For generic types
wrapping "return (T) o" with try-catch doesn't work because the type
gets erased to its bound at runtime, so the type mismatch escapes that
try-catch to the caller, potentially causing a crash. Now they happen
inside the getters, as the non-generic ones.

Test: Boots for now
Test: Working on CTS
Test: atest -d android.os.cts.ParcelTest android.os.cts.BundleTest android.os.BundleTest android.os.ParcelTest
CTS-Coverage-Bug: 219980813
Change-Id: Ifcbeb34b4684d7de105756b9d414162a9205ffaa
parent 32b2842e
Loading
Loading
Loading
Loading
+11 −6
Original line number Diff line number Diff line
@@ -29424,7 +29424,7 @@ package android.os {
  public class BaseBundle {
    method public void clear();
    method public boolean containsKey(String);
    method @Nullable public Object get(String);
    method @Deprecated @Nullable public Object get(String);
    method public boolean getBoolean(String);
    method public boolean getBoolean(String, boolean);
    method @Nullable public boolean[] getBooleanArray(@Nullable String);
@@ -29665,16 +29665,21 @@ package android.os {
    method public float getFloat(String, float);
    method @Nullable public float[] getFloatArray(@Nullable String);
    method @Nullable public java.util.ArrayList<java.lang.Integer> getIntegerArrayList(@Nullable String);
    method @Nullable public <T extends android.os.Parcelable> T getParcelable(@Nullable String);
    method @Nullable public android.os.Parcelable[] getParcelableArray(@Nullable String);
    method @Nullable public <T extends android.os.Parcelable> java.util.ArrayList<T> getParcelableArrayList(@Nullable String);
    method @Nullable public java.io.Serializable getSerializable(@Nullable String);
    method @Deprecated @Nullable public <T extends android.os.Parcelable> T getParcelable(@Nullable String);
    method @Nullable public <T> T getParcelable(@Nullable String, @NonNull Class<T>);
    method @Deprecated @Nullable public android.os.Parcelable[] getParcelableArray(@Nullable String);
    method @Nullable public <T> T[] getParcelableArray(@Nullable String, @NonNull Class<T>);
    method @Deprecated @Nullable public <T extends android.os.Parcelable> java.util.ArrayList<T> getParcelableArrayList(@Nullable String);
    method @Nullable public <T> java.util.ArrayList<T> getParcelableArrayList(@Nullable String, @NonNull Class<T>);
    method @Deprecated @Nullable public java.io.Serializable getSerializable(@Nullable String);
    method @Nullable public <T extends java.io.Serializable> T getSerializable(@Nullable String, @NonNull Class<T>);
    method public short getShort(String);
    method public short getShort(String, short);
    method @Nullable public short[] getShortArray(@Nullable String);
    method @Nullable public android.util.Size getSize(@Nullable String);
    method @Nullable public android.util.SizeF getSizeF(@Nullable String);
    method @Nullable public <T extends android.os.Parcelable> android.util.SparseArray<T> getSparseParcelableArray(@Nullable String);
    method @Deprecated @Nullable public <T extends android.os.Parcelable> android.util.SparseArray<T> getSparseParcelableArray(@Nullable String);
    method @Nullable public <T> android.util.SparseArray<T> getSparseParcelableArray(@Nullable String, @NonNull Class<T>);
    method @Nullable public java.util.ArrayList<java.lang.String> getStringArrayList(@Nullable String);
    method public boolean hasFileDescriptors();
    method public void putAll(android.os.Bundle);
+30 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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;

/** Used by Parcel to signal that the type on the payload was not expected by the caller. */
class BadTypeParcelableException extends BadParcelableException {
    BadTypeParcelableException(String msg) {
        super(msg);
    }
    BadTypeParcelableException(Exception cause) {
        super(cause);
    }
    BadTypeParcelableException(String msg, Throwable cause) {
        super(msg, cause);
    }
}
+103 −44
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package android.os;

import static java.util.Objects.requireNonNull;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
@@ -31,7 +33,7 @@ import com.android.internal.util.IndentingPrintWriter;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Set;
import java.util.function.Function;
import java.util.function.BiFunction;

/**
 * A mapping from String keys to values of various types. In most cases, you
@@ -254,8 +256,8 @@ public class BaseBundle {
        }
        try {
            return getValueAt(0, String.class);
        } catch (ClassCastException | BadParcelableException e) {
            typeWarning("getPairValue()", /* value */ null, "String", e);
        } catch (ClassCastException | BadTypeParcelableException e) {
            typeWarning("getPairValue()", "String", e);
            return null;
        }
    }
@@ -320,28 +322,46 @@ public class BaseBundle {
     * This call should always be made after {@link #unparcel()} or inside a lock after making sure
     * {@code mMap} is not null.
     *
     * @deprecated Use {@link #getValue(String, Class, Class[])}. This method should only be used in
     *      other deprecated APIs.
     *
     * @hide
     */
    @Deprecated
    @Nullable
    final Object getValue(String key) {
        return getValue(key, /* clazz */ null);
    }

    /** Same as {@link #getValue(String, Class, Class[])} with no item types. */
    @Nullable
    final <T> T getValue(String key, @Nullable Class<T> clazz) {
        // Avoids allocating Class[0] array
        return getValue(key, clazz, (Class<?>[]) null);
    }

    /**
     * Returns the value for key {@code key} for expected return type {@param clazz} (or {@code
     * Returns the value for key {@code key} for expected return type {@code clazz} (or pass {@code
     * null} for no type check).
     *
     * For {@code itemTypes}, see {@link Parcel#readValue(int, ClassLoader, Class, Class[])}.
     *
     * This call should always be made after {@link #unparcel()} or inside a lock after making sure
     * {@code mMap} is not null.
     *
     * @hide
     */
    final <T> T getValue(String key, @Nullable Class<T> clazz) {
    @Nullable
    final <T> T getValue(String key, @Nullable Class<T> clazz, @Nullable Class<?>... itemTypes) {
        int i = mMap.indexOfKey(key);
        return (i >= 0) ? getValueAt(i, clazz) : null;
        return (i >= 0) ? getValueAt(i, clazz, itemTypes) : null;
    }

    /**
     * Returns the value for a certain position in the array map.
     * Returns the value for a certain position in the array map for expected return type {@code
     * clazz} (or pass {@code null} for no type check).
     *
     * For {@code itemTypes}, see {@link Parcel#readValue(int, ClassLoader, Class, Class[])}.
     *
     * This call should always be made after {@link #unparcel()} or inside a lock after making sure
     * {@code mMap} is not null.
@@ -349,11 +369,12 @@ public class BaseBundle {
     * @hide
     */
    @SuppressWarnings("unchecked")
    final <T> T getValueAt(int i, @Nullable Class<T> clazz) {
    @Nullable
    final <T> T getValueAt(int i, @Nullable Class<T> clazz, @Nullable Class<?>... itemTypes) {
        Object object = mMap.valueAt(i);
        if (object instanceof Function<?, ?>) {
        if (object instanceof BiFunction<?, ?, ?>) {
            try {
                object = ((Function<Class<?>, ?>) object).apply(clazz);
                object = ((BiFunction<Class<?>, Class<?>[], ?>) object).apply(clazz, itemTypes);
            } catch (BadParcelableException e) {
                if (sShouldDefuse) {
                    Log.w(TAG, "Failed to parse item " + mMap.keyAt(i) + ", returning null.", e);
@@ -615,13 +636,43 @@ public class BaseBundle {
     *
     * @param key a String key
     * @return an Object, or null
     *
     * @deprecated Use the type-safe specific APIs depending on the type of the item to be
     *      retrieved, eg. {@link #getString(String)}.
     */
    @Deprecated
    @Nullable
    public Object get(String key) {
        unparcel();
        return getValue(key);
    }

    /**
     * Returns the object of type {@code clazz} for the given {@code key}, or {@code null} if:
     * <ul>
     *     <li>No mapping of the desired type exists for the given key.
     *     <li>A {@code null} value is explicitly associated with the key.
     *     <li>The object is not of type {@code clazz}.
     * </ul>
     *
     * <p>Use the more specific APIs where possible, especially in the case of containers such as
     * lists, since those APIs allow you to specify the type of the items.
     *
     * @param key String key
     * @param clazz The type of the object expected
     * @return an Object, or null
     */
    @Nullable
    <T> T get(@Nullable String key, @NonNull Class<T> clazz) {
        unparcel();
        try {
            return getValue(key, requireNonNull(clazz));
        } catch (ClassCastException | BadTypeParcelableException e) {
            typeWarning(key, clazz.getCanonicalName(), e);
            return null;
        }
    }

    /**
     * Removes any entry with the given key from the mapping of this Bundle.
     *
@@ -1019,6 +1070,10 @@ public class BaseBundle {
        typeWarning(key, value, className, "<null>", e);
    }

    void typeWarning(String key, String className, RuntimeException e) {
        typeWarning(key, /* value */ null, className, "<null>", e);
    }

    /**
     * Returns the value associated with the given key, or defaultValue if
     * no mapping of the desired type exists for the given key.
@@ -1358,7 +1413,11 @@ public class BaseBundle {
     *
     * @param key a String, or null
     * @return a Serializable value, or null
     *
     * @deprecated Use {@link #getSerializable(String, Class)}. This method should only be used in
     *      other deprecated APIs.
     */
    @Deprecated
    @Nullable
    Serializable getSerializable(@Nullable String key) {
        unparcel();
@@ -1374,6 +1433,36 @@ public class BaseBundle {
        }
    }

    /**
     * Returns the value associated with the given key, or {@code null} if:
     * <ul>
     *     <li>No mapping of the desired type exists for the given key.
     *     <li>A {@code null} value is explicitly associated with the key.
     *     <li>The object is not of type {@code clazz}.
     * </ul>
     *
     * @param key a String, or null
     * @param clazz The expected class of the returned type
     * @return a Serializable value, or null
     */
    @Nullable
    <T extends Serializable> T getSerializable(@Nullable String key, @NonNull Class<T> clazz) {
        return get(key, clazz);
    }


    @SuppressWarnings("unchecked")
    @Nullable
    <T> ArrayList<T> getArrayList(@Nullable String key, @NonNull Class<T> clazz) {
        unparcel();
        try {
            return getValue(key, ArrayList.class, requireNonNull(clazz));
        } catch (ClassCastException | BadTypeParcelableException e) {
            typeWarning(key, "ArrayList<" + clazz.getCanonicalName() + ">", e);
            return null;
        }
    }

    /**
     * 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
@@ -1384,17 +1473,7 @@ public class BaseBundle {
     */
    @Nullable
    ArrayList<Integer> getIntegerArrayList(@Nullable String key) {
        unparcel();
        Object o = getValue(key);
        if (o == null) {
            return null;
        }
        try {
            return (ArrayList<Integer>) o;
        } catch (ClassCastException e) {
            typeWarning(key, o, "ArrayList<Integer>", e);
            return null;
        }
        return getArrayList(key, Integer.class);
    }

    /**
@@ -1407,17 +1486,7 @@ public class BaseBundle {
     */
    @Nullable
    ArrayList<String> getStringArrayList(@Nullable String key) {
        unparcel();
        Object o = getValue(key);
        if (o == null) {
            return null;
        }
        try {
            return (ArrayList<String>) o;
        } catch (ClassCastException e) {
            typeWarning(key, o, "ArrayList<String>", e);
            return null;
        }
        return getArrayList(key, String.class);
    }

    /**
@@ -1430,17 +1499,7 @@ public class BaseBundle {
     */
    @Nullable
    ArrayList<CharSequence> getCharSequenceArrayList(@Nullable String key) {
        unparcel();
        Object o = getValue(key);
        if (o == null) {
            return null;
        }
        try {
            return (ArrayList<CharSequence>) o;
        } catch (ClassCastException e) {
            typeWarning(key, o, "ArrayList<CharSequence>", e);
            return null;
        }
        return getArrayList(key, CharSequence.class);
    }

    /**
+137 −15
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import static java.util.Objects.requireNonNull;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.compat.annotation.UnsupportedAppUsage;
import android.util.ArrayMap;
import android.util.Size;
@@ -876,7 +877,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
    @Nullable
    public Bundle getBundle(@Nullable String key) {
        unparcel();
        Object o = getValue(key);
        Object o = mMap.get(key);
        if (o == null) {
            return null;
        }
@@ -899,7 +900,11 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
     *
     * @param key a String, or {@code null}
     * @return a Parcelable value, or {@code null}
     *
     * @deprecated Use the type-safer {@link #getParcelable(String, Class)} starting from Android
     *      {@link Build.VERSION_CODES#TIRAMISU}.
     */
    @Deprecated
    @Nullable
    public <T extends Parcelable> T getParcelable(@Nullable String key) {
        unparcel();
@@ -916,30 +921,28 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
    }

    /**
     * Returns the value associated with the given key, or {@code null} if
     * no mapping of the desired type exists for the given key or a {@code null}
     * value is explicitly associated with the key.
     * Returns the value associated with the given key or {@code null} if:
     * <ul>
     *     <li>No mapping of the desired type exists for the given key.
     *     <li>A {@code null} value is explicitly associated with the key.
     *     <li>The object is not of type {@code clazz}.
     * </ul>
     *
     * <p><b>Note: </b> if the expected value is not a class provided by the Android platform,
     * you must call {@link #setClassLoader(ClassLoader)} with the proper {@link ClassLoader} first.
     * Otherwise, this method might throw an exception or return {@code null}.
     *
     * @param key a String, or {@code null}
     * @param clazz The type of the object expected or {@code null} for performing no checks.
     * @param clazz The type of the object expected
     * @return a Parcelable value, or {@code null}
     *
     * @hide
     */
    @SuppressWarnings("unchecked")
    @Nullable
    public <T> T getParcelable(@Nullable String key, @NonNull Class<T> clazz) {
        unparcel();
        try {
            return getValue(key, requireNonNull(clazz));
        } catch (ClassCastException | BadParcelableException e) {
            typeWarning(key, /* value */ null, "Parcelable", e);
            return null;
        }
        // The reason for not using <T extends Parcelable> is because the caller could provide a
        // super class to restrict the children that doesn't implement Parcelable itself while the
        // children do, more details at b/210800751 (same reasoning applies here).
        return get(key, clazz);
    }

    /**
@@ -953,7 +956,11 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
     *
     * @param key a String, or {@code null}
     * @return a Parcelable[] value, or {@code null}
     *
     * @deprecated Use the type-safer {@link #getParcelableArray(String, Class)} starting from
     *      Android {@link Build.VERSION_CODES#TIRAMISU}.
     */
    @Deprecated
    @Nullable
    public Parcelable[] getParcelableArray(@Nullable String key) {
        unparcel();
@@ -969,6 +976,39 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
        }
    }

    /**
     * Returns the value associated with the given key, or {@code null} if:
     * <ul>
     *     <li>No mapping of the desired type exists for the given key.
     *     <li>A {@code null} value is explicitly associated with the key.
     *     <li>The object is not of type {@code clazz}.
     * </ul>
     *
     * <p><b>Note: </b> if the expected value is not a class provided by the Android platform,
     * you must call {@link #setClassLoader(ClassLoader)} with the proper {@link ClassLoader} first.
     * Otherwise, this method might throw an exception or return {@code null}.
     *
     * @param key a String, or {@code null}
     * @param clazz The type of the items inside the array
     * @return a Parcelable[] value, or {@code null}
     */
    @SuppressLint({"ArrayReturn", "NullableCollection"})
    @SuppressWarnings("unchecked")
    @Nullable
    public <T> T[] getParcelableArray(@Nullable String key, @NonNull Class<T> clazz) {
        // The reason for not using <T extends Parcelable> is because the caller could provide a
        // super class to restrict the children that doesn't implement Parcelable itself while the
        // children do, more details at b/210800751 (same reasoning applies here).
        unparcel();
        try {
            // In Java 12, we can pass clazz.arrayType() instead of Parcelable[] and later casting.
            return (T[]) getValue(key, Parcelable[].class, requireNonNull(clazz));
        } catch (ClassCastException | BadTypeParcelableException e) {
            typeWarning(key, clazz.getCanonicalName() + "[]", e);
            return null;
        }
    }

    /**
     * Returns the value associated with the given key, or {@code null} if
     * no mapping of the desired type exists for the given key or a {@code null}
@@ -980,7 +1020,11 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
     *
     * @param key a String, or {@code null}
     * @return an ArrayList<T> value, or {@code null}
     *
     * @deprecated Use the type-safer {@link #getParcelable(String, Class)} starting from Android
     *      {@link Build.VERSION_CODES#TIRAMISU}.
     */
    @Deprecated
    @Nullable
    public <T extends Parcelable> ArrayList<T> getParcelableArrayList(@Nullable String key) {
        unparcel();
@@ -996,15 +1040,44 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
        }
    }

    /**
     * Returns the value associated with the given key, or {@code null} if:
     * <ul>
     *     <li>No mapping of the desired type exists for the given key.
     *     <li>A {@code null} value is explicitly associated with the key.
     *     <li>The object is not of type {@code clazz}.
     * </ul>
     *
     * <p><b>Note: </b> if the expected value is not a class provided by the Android platform,
     * you must call {@link #setClassLoader(ClassLoader)} with the proper {@link ClassLoader} first.
     * Otherwise, this method might throw an exception or return {@code null}.
     *
     * @param key   a String, or {@code null}
     * @param clazz The type of the items inside the array list
     * @return an ArrayList<T> value, or {@code null}
     */
    @SuppressLint("NullableCollection")
    @SuppressWarnings("unchecked")
    @Nullable
    public <T> ArrayList<T> getParcelableArrayList(@Nullable String key, @NonNull Class<T> clazz) {
        // The reason for not using <T extends Parcelable> is because the caller could provide a
        // super class to restrict the children that doesn't implement Parcelable itself while the
        // children do, more details at b/210800751 (same reasoning applies here).
        return getArrayList(key, clazz);
    }

    /**
     * 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 SparseArray of T values, or null
     *
     * @deprecated Use the type-safer {@link #getSparseParcelableArray(String, Class)} starting from
     *      Android {@link Build.VERSION_CODES#TIRAMISU}.
     */
    @Deprecated
    @Nullable
    public <T extends Parcelable> SparseArray<T> getSparseParcelableArray(@Nullable String key) {
        unparcel();
@@ -1020,6 +1093,33 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
        }
    }

    /**
     * Returns the value associated with the given key, or {@code null} if:
     * <ul>
     *     <li>No mapping of the desired type exists for the given key.
     *     <li>A {@code null} value is explicitly associated with the key.
     *     <li>The object is not of type {@code clazz}.
     * </ul>
     *
     * @param key a String, or null
     * @return a SparseArray of T values, or null
     */
    @SuppressWarnings("unchecked")
    @Nullable
    public <T> SparseArray<T> getSparseParcelableArray(@Nullable String key,
            @NonNull Class<T> clazz) {
        // The reason for not using <T extends Parcelable> is because the caller could provide a
        // super class to restrict the children that doesn't implement Parcelable itself while the
        // children do, more details at b/210800751 (same reasoning applies here).
        unparcel();
        try {
            return (SparseArray<T>) getValue(key, SparseArray.class, requireNonNull(clazz));
        } catch (ClassCastException | BadTypeParcelableException e) {
            typeWarning(key, "SparseArray<" + clazz.getCanonicalName() + ">", e);
            return null;
        }
    }

    /**
     * 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
@@ -1027,13 +1127,35 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
     *
     * @param key a String, or null
     * @return a Serializable value, or null
     *
     * @deprecated Use the type-safer {@link #getSerializable(String, Class)} starting from Android
     *      {@link Build.VERSION_CODES#TIRAMISU}.
     */
    @Deprecated
    @Override
    @Nullable
    public Serializable getSerializable(@Nullable String key) {
        return super.getSerializable(key);
    }

    /**
     * Returns the value associated with the given key, or {@code null} if:
     * <ul>
     *     <li>No mapping of the desired type exists for the given key.
     *     <li>A {@code null} value is explicitly associated with the key.
     *     <li>The object is not of type {@code clazz}.
     * </ul>
     *
     * @param key   a String, or null
     * @param clazz The expected class of the returned type
     * @return a Serializable value, or null
     */
    @Nullable
    public <T extends Serializable> T getSerializable(@Nullable String key,
            @NonNull Class<T> clazz) {
        return super.getSerializable(key, requireNonNull(clazz));
    }

    /**
     * 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
+134 −71

File changed.

Preview size limit exceeded, changes collapsed.

Loading