Add runtime unmodifiable check for PackageState
Introduces test that enforces that all members and their type
parameter children types that return collections/maps return non-null,
non-empty, unmodifiable objects when a test instance is constructed
and filled with data.
Makes the PackageImpl collections unmodifiable, but ignores the inner
objects with a constants list in the test. To be removed once the
inner object infrastructure is finished and can be marked immutable.
Bug: 235462722
Test: atest PackageManagerServiceUnitTests
Change-Id: I9759b7dd5c945e65498e22fcf408d53931dc502f
diff --git a/services/core/java/com/android/server/pm/ApexPackageInfo.java b/services/core/java/com/android/server/pm/ApexPackageInfo.java
index 4dd9c49..672ae2e 100644
--- a/services/core/java/com/android/server/pm/ApexPackageInfo.java
+++ b/services/core/java/com/android/server/pm/ApexPackageInfo.java
@@ -329,17 +329,15 @@
ApexInfo ai = parsingApexInfo.get(parseResult.scanFile);
if (throwable == null) {
- // Calling hideAsFinal to assign derived fields for the app info flags.
- parseResult.parsedPackage.hideAsFinal();
-
// TODO: When ENABLE_FEATURE_SCAN_APEX is finalized, remove this and the entire
// calling path code
ScanPackageUtils.applyPolicy(parseResult.parsedPackage,
PackageManagerService.SCAN_AS_SYSTEM,
mPackageManager == null ? null : mPackageManager.getPlatformPackage(),
false);
- results.add(new ApexManager.ScanResult(
- ai, parseResult.parsedPackage, parseResult.parsedPackage.getPackageName()));
+ // Calling hideAsFinal to assign derived fields for the app info flags.
+ AndroidPackage finalPkg = parseResult.parsedPackage.hideAsFinal();
+ results.add(new ApexManager.ScanResult(ai, finalPkg, finalPkg.getPackageName()));
} else if (throwable instanceof PackageManagerException) {
throw new IllegalStateException("Unable to parse: " + ai.modulePath, throwable);
} else {
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index a25ee5e..f6e5b69 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -3451,8 +3451,8 @@
if (throwable == null) {
try {
- AndroidPackage pkg = addForInitLI(
- parseResult.parsedPackage, newParseFlags, newScanFlags, null);
+ addForInitLI(parseResult.parsedPackage, newParseFlags, newScanFlags, null);
+ AndroidPackage pkg = parseResult.parsedPackage.hideAsFinal();
if (ai.isFactory && !ai.isActive) {
disableSystemPackageLPw(pkg);
}
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 4764a5c..03f17bd 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -1208,11 +1208,23 @@
}
@NonNull
+ public PackageSetting addUsesLibraryInfo(@NonNull SharedLibraryInfo value) {
+ pkgState.addUsesLibraryInfo(value);
+ return this;
+ }
+
+ @NonNull
@Override
public List<String> getUsesLibraryFiles() {
return pkgState.getUsesLibraryFiles();
}
+ @NonNull
+ public PackageSetting addUsesLibraryFile(String value) {
+ pkgState.addUsesLibraryFile(value);
+ return this;
+ }
+
@Override
public boolean isHiddenUntilInstalled() {
return pkgState.isHiddenUntilInstalled();
diff --git a/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java b/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java
index 70aca99..fe63dec 100644
--- a/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java
+++ b/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java
@@ -385,16 +385,16 @@
@Nullable
@DataClass.ParcelWith(Parcelling.BuiltIn.ForBoolean.class)
private Boolean requestRawExternalStorageAccess;
- // TODO(chiuwinson): Non-null
- @Nullable
- private ArraySet<String> mimeGroups;
+ @NonNull
+ @DataClass.ParcelWith(Parcelling.BuiltIn.ForInternedStringSet.class)
+ private Set<String> mimeGroups = emptySet();
// Usually there's code to set enabled to true during parsing, but it's possible to install
// an APK targeting <R that doesn't contain an <application> tag. That code would be skipped
// and never assign this, so initialize this to true for those cases.
private long mBooleans = Booleans.ENABLED;
private long mBooleans2;
- @Nullable
- private Set<String> mKnownActivityEmbeddingCerts;
+ @NonNull
+ private Set<String> mKnownActivityEmbeddingCerts = emptySet();
// Derived fields
private long mLongVersionCode;
private int mLocaleConfigRes;
@@ -549,7 +549,7 @@
if (mimeGroups != null && mimeGroups.size() > 500) {
throw new IllegalStateException("Max limit on number of MIME Groups reached");
}
- mimeGroups = ArrayUtils.add(mimeGroups, filter.getMimeGroup(groupIndex));
+ mimeGroups = CollectionUtils.add(mimeGroups, filter.getMimeGroup(groupIndex));
}
}
}
@@ -935,8 +935,7 @@
@NonNull
@Override
public Set<String> getKnownActivityEmbeddingCerts() {
- return mKnownActivityEmbeddingCerts == null ? Collections.emptySet()
- : mKnownActivityEmbeddingCerts;
+ return mKnownActivityEmbeddingCerts;
}
@Override
@@ -1949,8 +1948,7 @@
}
@Override
- public ParsingPackage setKnownActivityEmbeddingCerts(
- @Nullable Set<String> knownEmbeddingCerts) {
+ public ParsingPackage setKnownActivityEmbeddingCerts(@NonNull Set<String> knownEmbeddingCerts) {
mKnownActivityEmbeddingCerts = knownEmbeddingCerts;
return this;
}
@@ -2516,7 +2514,7 @@
appInfo.setVersionCode(mLongVersionCode);
appInfo.setAppClassNamesByProcess(buildAppClassNamesByProcess());
appInfo.setLocaleConfigRes(mLocaleConfigRes);
- if (mKnownActivityEmbeddingCerts != null) {
+ if (!mKnownActivityEmbeddingCerts.isEmpty()) {
appInfo.setKnownActivityEmbeddingCerts(mKnownActivityEmbeddingCerts);
}
@@ -2593,11 +2591,11 @@
@Override
public AndroidPackageInternal hideAsFinal() {
- // TODO(b/135203078): Lock as immutable
if (mStorageUuid == null) {
assignDerivedFields();
}
assignDerivedFields2();
+ makeImmutable();
return this;
}
@@ -2613,6 +2611,48 @@
baseAppDataDir + Environment.DIR_USER_DE + systemUserSuffix);
}
+ private void makeImmutable() {
+ usesLibraries = Collections.unmodifiableList(usesLibraries);
+ usesOptionalLibraries = Collections.unmodifiableList(usesOptionalLibraries);
+ usesNativeLibraries = Collections.unmodifiableList(usesNativeLibraries);
+ usesOptionalNativeLibraries = Collections.unmodifiableList(usesOptionalNativeLibraries);
+ originalPackages = Collections.unmodifiableList(originalPackages);
+ adoptPermissions = Collections.unmodifiableList(adoptPermissions);
+ requestedPermissions = Collections.unmodifiableList(requestedPermissions);
+ protectedBroadcasts = Collections.unmodifiableList(protectedBroadcasts);
+ apexSystemServices = Collections.unmodifiableList(apexSystemServices);
+
+ activities = Collections.unmodifiableList(activities);
+ receivers = Collections.unmodifiableList(receivers);
+ services = Collections.unmodifiableList(services);
+ providers = Collections.unmodifiableList(providers);
+ permissions = Collections.unmodifiableList(permissions);
+ permissionGroups = Collections.unmodifiableList(permissionGroups);
+ instrumentations = Collections.unmodifiableList(instrumentations);
+
+ overlayables = Collections.unmodifiableMap(overlayables);
+ libraryNames = Collections.unmodifiableList(libraryNames);
+ usesStaticLibraries = Collections.unmodifiableList(usesStaticLibraries);
+ usesSdkLibraries = Collections.unmodifiableList(usesSdkLibraries);
+ configPreferences = Collections.unmodifiableList(configPreferences);
+ reqFeatures = Collections.unmodifiableList(reqFeatures);
+ featureGroups = Collections.unmodifiableList(featureGroups);
+ usesPermissions = Collections.unmodifiableList(usesPermissions);
+ usesSdkLibraries = Collections.unmodifiableList(usesSdkLibraries);
+ implicitPermissions = Collections.unmodifiableList(implicitPermissions);
+ upgradeKeySets = Collections.unmodifiableSet(upgradeKeySets);
+ keySetMapping = Collections.unmodifiableMap(keySetMapping);
+ attributions = Collections.unmodifiableList(attributions);
+ preferredActivityFilters = Collections.unmodifiableList(preferredActivityFilters);
+ processes = Collections.unmodifiableMap(processes);
+ mProperties = Collections.unmodifiableMap(mProperties);
+ queriesIntents = Collections.unmodifiableList(queriesIntents);
+ queriesPackages = Collections.unmodifiableList(queriesPackages);
+ queriesProviders = Collections.unmodifiableSet(queriesProviders);
+ mimeGroups = Collections.unmodifiableSet(mimeGroups);
+ mKnownActivityEmbeddingCerts = Collections.unmodifiableSet(mKnownActivityEmbeddingCerts);
+ }
+
@Override
public long getLongVersionCode() {
return PackageInfo.composeLongVersionCode(versionCodeMajor, versionCode);
@@ -3041,7 +3081,7 @@
dest.writeIntArray(this.splitRevisionCodes);
sForBoolean.parcel(this.resizeableActivity, dest, flags);
dest.writeInt(this.autoRevokePermissions);
- dest.writeArraySet(this.mimeGroups);
+ sForInternedStringSet.parcel(this.mimeGroups, dest, flags);
dest.writeInt(this.gwpAsanMode);
dest.writeSparseIntArray(this.minExtensionVersions);
dest.writeMap(this.mProperties);
@@ -3201,7 +3241,7 @@
this.resizeableActivity = sForBoolean.unparcel(in);
this.autoRevokePermissions = in.readInt();
- this.mimeGroups = (ArraySet<String>) in.readArraySet(boot);
+ this.mimeGroups = sForInternedStringSet.unparcel(in);
this.gwpAsanMode = in.readInt();
this.minExtensionVersions = in.readSparseIntArray();
this.mProperties = in.readHashMap(boot);
@@ -3224,6 +3264,9 @@
assignDerivedFields();
assignDerivedFields2();
+
+ // Do not call makeImmutable here as cached parsing will need
+ // to mutate this instance before it's finalized.
}
@NonNull
diff --git a/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java b/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
index eac0842..28309c7 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
@@ -33,6 +33,7 @@
import com.android.server.pm.Settings;
import java.io.File;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -170,7 +171,7 @@
mLastModifiedTime = pkgState.getLastModifiedTime();
mLastUpdateTime = pkgState.getLastUpdateTime();
mLongVersionCode = pkgState.getVersionCode();
- mMimeGroups = pkgState.getMimeGroups();
+ mMimeGroups = Collections.unmodifiableMap(pkgState.getMimeGroups());
mPath = pkgState.getPath();
mPrimaryCpuAbi = pkgState.getPrimaryCpuAbi();
mSecondaryCpuAbi = pkgState.getSecondaryCpuAbi();
@@ -180,8 +181,8 @@
mUsesSdkLibrariesVersionsMajor = pkgState.getUsesSdkLibrariesVersionsMajor();
mUsesStaticLibraries = pkgState.getUsesStaticLibraries();
mUsesStaticLibrariesVersions = pkgState.getUsesStaticLibrariesVersions();
- mUsesLibraryInfos = pkgState.getUsesLibraryInfos();
- mUsesLibraryFiles = pkgState.getUsesLibraryFiles();
+ mUsesLibraryInfos = Collections.unmodifiableList(pkgState.getUsesLibraryInfos());
+ mUsesLibraryFiles = Collections.unmodifiableList(pkgState.getUsesLibraryFiles());
setBoolean(Booleans.FORCE_QUERYABLE_OVERRIDE, pkgState.isForceQueryableOverride());
setBoolean(Booleans.HIDDEN_UNTIL_INSTALLED, pkgState.isHiddenUntilInstalled());
setBoolean(Booleans.INSTALL_PERMISSIONS_FIXED, pkgState.isInstallPermissionsFixed());
@@ -195,8 +196,8 @@
int userStatesSize = userStates.size();
mUserStates = new SparseArray<>(userStatesSize);
for (int index = 0; index < userStatesSize; index++) {
- mUserStates.put(mUserStates.keyAt(index),
- UserStateImpl.copy(mUserStates.valueAt(index)));
+ mUserStates.put(userStates.keyAt(index),
+ UserStateImpl.copy(userStates.valueAt(index)));
}
}
diff --git a/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java b/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java
index 2c8b977..e4eed5f 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java
@@ -23,6 +23,7 @@
import android.content.pm.PackageManager;
import android.content.pm.SharedLibraryInfo;
+import com.android.internal.util.CollectionUtils;
import com.android.internal.util.DataClass;
import com.android.server.pm.PackageSetting;
@@ -69,6 +70,18 @@
mPackageSetting = packageSetting;
}
+ @NonNull
+ public PackageStateUnserialized addUsesLibraryInfo(@NonNull SharedLibraryInfo value) {
+ usesLibraryInfos = CollectionUtils.add(usesLibraryInfos, value);
+ return this;
+ }
+
+ @NonNull
+ public PackageStateUnserialized addUsesLibraryFile(@NonNull String value) {
+ usesLibraryFiles = CollectionUtils.add(usesLibraryFiles, value);
+ return this;
+ }
+
private long[] lazyInitLastPackageUsageTimeInMills() {
return new long[PackageManager.NOTIFY_PACKAGE_USE_REASONS_COUNT];
}
diff --git a/services/tests/PackageManagerServiceTests/unit/Android.bp b/services/tests/PackageManagerServiceTests/unit/Android.bp
index 1bcc3d1..1c6ba33 100644
--- a/services/tests/PackageManagerServiceTests/unit/Android.bp
+++ b/services/tests/PackageManagerServiceTests/unit/Android.bp
@@ -35,6 +35,7 @@
"kotlin-reflect",
"services.core",
"servicestests-utils",
+ "servicestests-core-utils",
"truth-prebuilt",
],
platform_apis: true,
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt
index 5361041..b41fd39 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt
@@ -23,7 +23,6 @@
import android.content.pm.FeatureInfo
import android.content.pm.PackageManager
import android.content.pm.SigningDetails
-import com.android.server.pm.pkg.parsing.ParsingPackage
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
@@ -65,6 +64,7 @@
"addMimeGroupsFromComponent",
"assignDerivedFields",
"assignDerivedFields2",
+ "makeImmutable",
"buildFakeForDeletion",
"buildAppClassNamesByProcess",
"capPermissionPriorities",
@@ -219,6 +219,7 @@
AndroidPackage::isNativeLibraryRootRequiresIsa,
AndroidPackage::isOdm,
AndroidPackage::isOem,
+ AndroidPackage::isOnBackInvokedCallbackEnabled,
AndroidPackage::isOverlay,
AndroidPackage::isOverlayIsStatic,
AndroidPackage::isPartiallyDirectBootAware,
@@ -279,7 +280,7 @@
adder(AndroidPackage::getUsesOptionalNativeLibraries, "testUsesOptionalNativeLibrary"),
getSetByValue(
AndroidPackage::areAttributionsUserVisible,
- ParsingPackage::setAttributionsAreUserVisible,
+ PackageImpl::setAttributionsAreUserVisible,
true
),
getSetByValue2(
@@ -513,11 +514,6 @@
}
),
getter(AndroidPackage::getKnownActivityEmbeddingCerts, setOf("TESTEMBEDDINGCERT")),
- getSetByValue(
- AndroidPackage::isOnBackInvokedCallbackEnabled,
- ParsingPackage::setOnBackInvokedCallbackEnabled,
- true
- )
)
override fun initialObject() = PackageImpl.forParsing(
@@ -560,10 +556,13 @@
.setSplitHasCode(1, false)
.setSplitClassLoaderName(0, "testSplitClassLoaderNameZero")
.setSplitClassLoaderName(1, "testSplitClassLoaderNameOne")
-
.addUsesSdkLibrary("testSdk", 2L, arrayOf("testCertDigest1"))
.addUsesStaticLibrary("testStatic", 3L, arrayOf("testCertDigest2"))
+ override fun finalizeObject(parcelable: Parcelable) {
+ (parcelable as PackageImpl).hideAsParsed().hideAsFinal()
+ }
+
override fun extraAssertions(before: Parcelable, after: Parcelable) {
super.extraAssertions(before, after)
after as PackageImpl
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParcelableComponentTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParcelableComponentTest.kt
index e16a187..37bb935 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParcelableComponentTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParcelableComponentTest.kt
@@ -293,12 +293,21 @@
first?.let { property(it) } == second?.let { property(it) }
}
- @Test
- fun valueComparison() {
+ fun buildBefore(): Pair<List<Param>, Parcelable> {
val params = baseParams.mapNotNull(::buildParams) + extraParams().filterNotNull()
val before = initialObject()
params.forEach { it.setFunction(arrayOf(before, it.value())) }
+ finalizeObject(before)
+ return params to before
+ }
+
+ protected open fun finalizeObject(parcelable: Parcelable) {
+ }
+
+ @Test
+ fun valueComparison() {
+ val (params, before) = buildBefore()
val parcel = Parcel.obtain()
writeToParcel(parcel, before)
@@ -307,6 +316,15 @@
parcel.setDataPosition(0)
+ val baseline = initialObject()
+ finalizeObject(baseline)
+
+ val baselineParcel = Parcel.obtain()
+ writeToParcel(baselineParcel, baseline)
+
+ // Check that something substantial actually changed in the test object
+ expect.that(parcel.dataSize()).isGreaterThan(baselineParcel.dataSize())
+
val after = creator.createFromParcel(parcel)
expect.withMessage("Mismatched write and read data sizes")
@@ -314,6 +332,7 @@
.isEqualTo(dataSize)
parcel.recycle()
+ baselineParcel.recycle()
runAssertions(params, before, after)
}
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt
new file mode 100644
index 0000000..7e9e433
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt
@@ -0,0 +1,297 @@
+/*
+ * 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 com.android.server.pm.test.pkg
+
+import android.content.Intent
+import android.content.pm.overlay.OverlayPaths
+import android.content.pm.PackageManager
+import android.content.pm.PathPermission
+import android.content.pm.SharedLibraryInfo
+import android.content.pm.VersionedPackage
+import android.os.PatternMatcher
+import android.util.ArraySet
+import com.android.server.pm.PackageSetting
+import com.android.server.pm.PackageSettingBuilder
+import com.android.server.pm.parsing.pkg.PackageImpl
+import com.android.server.pm.pkg.AndroidPackage
+import com.android.server.pm.pkg.PackageState
+import com.android.server.pm.pkg.PackageStateImpl
+import com.android.server.pm.pkg.PackageUserState
+import com.android.server.pm.pkg.PackageUserStateImpl
+import com.android.server.pm.pkg.component.ParsedActivity
+import com.android.server.pm.pkg.component.ParsedActivityImpl
+import com.android.server.pm.pkg.component.ParsedComponentImpl
+import com.android.server.pm.pkg.component.ParsedInstrumentation
+import com.android.server.pm.pkg.component.ParsedIntentInfoImpl
+import com.android.server.pm.pkg.component.ParsedPermission
+import com.android.server.pm.pkg.component.ParsedPermissionGroup
+import com.android.server.pm.pkg.component.ParsedPermissionImpl
+import com.android.server.pm.pkg.component.ParsedProcess
+import com.android.server.pm.pkg.component.ParsedProcessImpl
+import com.android.server.pm.pkg.component.ParsedProvider
+import com.android.server.pm.pkg.component.ParsedProviderImpl
+import com.android.server.pm.pkg.component.ParsedService
+import com.android.server.pm.test.parsing.parcelling.AndroidPackageTest
+import com.google.common.truth.Expect
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import kotlin.contracts.ExperimentalContracts
+import kotlin.reflect.KClass
+import kotlin.reflect.KFunction
+import kotlin.reflect.KType
+import kotlin.reflect.full.isSubtypeOf
+import kotlin.reflect.full.memberFunctions
+import kotlin.reflect.full.starProjectedType
+
+class PackageStateTest {
+
+ companion object {
+ private val IGNORED_TYPES = listOf(
+ "java.io.File",
+ "java.lang.Boolean",
+ "java.lang.Byte",
+ "java.lang.CharSequence",
+ "java.lang.Character",
+ "java.lang.Double",
+ "java.lang.Float",
+ "java.lang.Integer",
+ "java.lang.Long",
+ "java.lang.Short",
+ "java.lang.String",
+ "java.lang.Void",
+ )
+ // STOPSHIP: Remove these and fix the implementations
+ private val IGNORED_FUNCTIONS = listOf(
+ ParsedActivity::getIntents,
+ ParsedActivity::getKnownActivityEmbeddingCerts,
+ ParsedActivity::getProperties,
+ ParsedInstrumentation::getIntents,
+ ParsedInstrumentation::getIntents,
+ ParsedInstrumentation::getProperties,
+ ParsedInstrumentation::getProperties,
+ ParsedPermission::getIntents,
+ ParsedPermission::getProperties,
+ ParsedPermissionGroup::getIntents,
+ ParsedPermissionGroup::getProperties,
+ ParsedProcess::getAppClassNamesByPackage,
+ ParsedProvider::getIntents,
+ ParsedProvider::getPathPermissions,
+ ParsedProvider::getProperties,
+ ParsedProvider::getUriPermissionPatterns,
+ ParsedService::getIntents,
+ ParsedService::getProperties,
+ SharedLibraryInfo::getAllCodePaths,
+ SharedLibraryInfo::getDependencies,
+ Intent::getCategories,
+ PackageUserState::getDisabledComponents,
+ PackageUserState::getEnabledComponents,
+ PackageUserState::getSharedLibraryOverlayPaths,
+ OverlayPaths::getOverlayPaths,
+ OverlayPaths::getResourceDirs,
+ )
+ }
+
+ @get:Rule
+ val tempFolder = TemporaryFolder()
+
+ @get:Rule
+ val expect = Expect.create()
+
+ private val collectionType = MutableCollection::class.starProjectedType
+ private val mapType = Map::class.starProjectedType
+
+ @OptIn(ExperimentalContracts::class)
+ @Test
+ fun collectionImmutability() {
+ val seenTypes = mutableSetOf<KType>()
+ val (_, pkg) = AndroidPackageTest().buildBefore()
+ val packageState = PackageSettingBuilder()
+ .setPackage(pkg as AndroidPackage)
+ .setCodePath(tempFolder.newFile().path)
+ .build()
+
+ fillMissingData(packageState, pkg as PackageImpl)
+
+ visitType(seenTypes, emptyList(), PackageStateImpl.copy(packageState),
+ PackageState::class.starProjectedType)
+ visitType(seenTypes, emptyList(), pkg, AndroidPackage::class.starProjectedType)
+ visitType(seenTypes, emptyList(), packageState.getUserStateOrDefault(0),
+ PackageUserState::class.starProjectedType)
+
+ // Don't check empties for defaults since their collections will always be empty
+ visitType(seenTypes, emptyList(), PackageUserState.DEFAULT,
+ PackageUserState::class.starProjectedType, enforceNonEmpty = false)
+
+ // Check that some minimum number of functions were validated,
+ // in case the type checking breaks somehow
+ expect.that(seenTypes.size).isGreaterThan(10)
+ }
+
+ /**
+ * Fill fields in [PackageState] and its children that are not filled by [AndroidPackageTest].
+ * Real objects and real invocations of the live APIs are necessary to ensure that the test
+ * mirrors real device behavior.
+ */
+ private fun fillMissingData(pkgSetting: PackageSetting, pkg: PackageImpl) {
+ pkgSetting.addUsesLibraryFile("usesLibraryFile")
+ pkgSetting.addUsesLibraryInfo(SharedLibraryInfo(
+ "path",
+ "packageName",
+ listOf(tempFolder.newFile().path),
+ "name",
+ 1,
+ 0,
+ VersionedPackage("versionedPackage0", 1),
+ listOf(VersionedPackage("versionedPackage1", 2)),
+ emptyList(),
+ false
+ ))
+ pkgSetting.addMimeTypes("mimeGroup", setOf("mimeType"))
+ pkgSetting.getOrCreateUserState(0).apply {
+ setEnabledComponents(ArraySet<String>().apply { add("com.test.EnabledComponent") })
+ setDisabledComponents(ArraySet<String>().apply { add("com.test.DisabledComponent") })
+ setSharedLibraryOverlayPaths("sharedLibrary",
+ OverlayPaths.Builder().addApkPath("/test/overlay.apk").build())
+ }
+
+ val property = PackageManager.Property("propertyName", 1, "com.test", null)
+ listOf(
+ pkg.activities,
+ pkg.receivers,
+ pkg.providers,
+ pkg.services,
+ pkg.instrumentations,
+ pkg.permissions,
+ pkg.permissionGroups
+ ).map { it.first() as ParsedComponentImpl }
+ .forEach {
+ it.addIntent(ParsedIntentInfoImpl())
+ it.addProperty(property)
+ }
+
+ (pkg.activities.first() as ParsedActivityImpl).knownActivityEmbeddingCerts =
+ setOf("TESTEMBEDDINGCERT")
+
+ (pkg.permissions.first() as ParsedPermissionImpl).knownCerts = setOf("TESTEMBEDDINGCERT")
+
+ (pkg.providers.first() as ParsedProviderImpl).apply {
+ addPathPermission(PathPermission("pattern", PatternMatcher.PATTERN_LITERAL,
+ "readPermission", "writerPermission"))
+ addUriPermissionPattern(PatternMatcher("*", PatternMatcher.PATTERN_LITERAL))
+ }
+
+ (pkg.processes.values.first() as ParsedProcessImpl).apply {
+ deniedPermissions = setOf("deniedPermission")
+ putAppClassNameForPackage("package", "className")
+ }
+ }
+
+ private fun visitType(
+ seenTypes: MutableSet<KType>,
+ parentChain: List<String>,
+ impl: Any,
+ type: KType,
+ enforceNonEmpty: Boolean = true
+ ) {
+ if (!seenTypes.add(type)) return
+ val kClass = type.classifier as KClass<*>
+ val qualifiedName = kClass.qualifiedName!!
+ if (IGNORED_TYPES.contains(qualifiedName)) return
+
+ val newChain = parentChain + kClass.simpleName!!
+ val newChainText = newChain.joinToString()
+
+ val filteredFunctions = kClass.memberFunctions
+ .filter {
+ // Size 1 because the impl receiver counts as a parameter
+ it.parameters.size == 1
+ }
+ .filterNot(IGNORED_FUNCTIONS::contains)
+
+ filteredFunctions.filter { it.returnType.isSubtypeOf(collectionType) }
+ .forEach {
+ val collection = it.call(impl)
+ if (collection as? MutableCollection<*> == null) {
+ expect.withMessage("Method $newChainText ${it.name} cannot return null")
+ .fail()
+ return@forEach
+ }
+
+ val value = try {
+ collection.stream().findFirst().get()!!
+ } catch (e: Exception) {
+ if (enforceNonEmpty) {
+ expect.withMessage("Method $newChainText ${it.name} returns empty")
+ .that(e)
+ .isNull()
+ return@forEach
+ } else null
+ }
+
+ if (value != null) {
+ it.returnType.arguments.forEach {
+ visitType(seenTypes, newChain, value, it.type!!)
+ }
+ }
+
+ // Must test clear last in case it works and actually clears the collection
+ expectUnsupported(newChain, it) { collection.clear() }
+ }
+ filteredFunctions.filter { it.returnType.isSubtypeOf(mapType) }
+ .forEach {
+ val map = it.call(impl)
+ if (map as? MutableMap<*, *> == null) {
+ expect.withMessage("Method $newChainText ${it.name} cannot return null")
+ .fail()
+ return@forEach
+ }
+
+ val entry = try {
+ map.entries.stream().findFirst().get()!!
+ } catch (e: Exception) {
+ expect.withMessage("Method $newChainText ${it.name} returns empty")
+ .that(e)
+ .isNull()
+ return@forEach
+ }
+
+ visitType(seenTypes, newChain, entry.key!!, it.returnType.arguments[0].type!!)
+ visitType(seenTypes, newChain, entry.value!!, it.returnType.arguments[1].type!!)
+
+ // Must test clear last in case it works and actually clears the map
+ expectUnsupported(newChain, it) { map.clear() }
+ }
+ }
+
+ private fun expectUnsupported(
+ parentChain: List<String>,
+ function: KFunction<*>,
+ block: () -> Unit
+ ) {
+ val exception = try {
+ block()
+ null
+ } catch (e: UnsupportedOperationException) {
+ e
+ }
+
+ expect.withMessage("Method ${parentChain.joinToString()} $function doesn't throw")
+ .that(exception)
+ .isNotNull()
+ }
+}
\ No newline at end of file