diff --git a/services/core/java/com/android/server/pm/ApexPackageInfo.java b/services/core/java/com/android/server/pm/ApexPackageInfo.java index 4dd9c49321b2d..672ae2eb0fb26 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 @@ class ApexPackageInfo { 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 a25ee5e11e7ea..f6e5b690fd2e4 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 @@ final class InstallPackageHelper { 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 4764a5c41db43..03f17bde88862 100644 --- a/services/core/java/com/android/server/pm/PackageSetting.java +++ b/services/core/java/com/android/server/pm/PackageSetting.java @@ -1207,12 +1207,24 @@ public class PackageSetting extends SettingBase implements PackageStateInternal return pkgState.getUsesLibraryInfos(); } + @NonNull + public PackageSetting addUsesLibraryInfo(@NonNull SharedLibraryInfo value) { + pkgState.addUsesLibraryInfo(value); + return this; + } + @NonNull @Override public List 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 70aca99f04e56..fe63deccec4b2 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 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, @Nullable @DataClass.ParcelWith(Parcelling.BuiltIn.ForBoolean.class) private Boolean requestRawExternalStorageAccess; - // TODO(chiuwinson): Non-null - @Nullable - private ArraySet mimeGroups; + @NonNull + @DataClass.ParcelWith(Parcelling.BuiltIn.ForInternedStringSet.class) + private Set mimeGroups = emptySet(); // Usually there's code to set enabled to true during parsing, but it's possible to install // an APK targeting 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 mKnownActivityEmbeddingCerts; + @NonNull + private Set mKnownActivityEmbeddingCerts = emptySet(); // Derived fields private long mLongVersionCode; private int mLocaleConfigRes; @@ -549,7 +549,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, 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 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, @NonNull @Override public Set getKnownActivityEmbeddingCerts() { - return mKnownActivityEmbeddingCerts == null ? Collections.emptySet() - : mKnownActivityEmbeddingCerts; + return mKnownActivityEmbeddingCerts; } @Override @@ -1949,8 +1948,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public ParsingPackage setKnownActivityEmbeddingCerts( - @Nullable Set knownEmbeddingCerts) { + public ParsingPackage setKnownActivityEmbeddingCerts(@NonNull Set knownEmbeddingCerts) { mKnownActivityEmbeddingCerts = knownEmbeddingCerts; return this; } @@ -2516,7 +2514,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, appInfo.setVersionCode(mLongVersionCode); appInfo.setAppClassNamesByProcess(buildAppClassNamesByProcess()); appInfo.setLocaleConfigRes(mLocaleConfigRes); - if (mKnownActivityEmbeddingCerts != null) { + if (!mKnownActivityEmbeddingCerts.isEmpty()) { appInfo.setKnownActivityEmbeddingCerts(mKnownActivityEmbeddingCerts); } @@ -2593,11 +2591,11 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, @Override public AndroidPackageInternal hideAsFinal() { - // TODO(b/135203078): Lock as immutable if (mStorageUuid == null) { assignDerivedFields(); } assignDerivedFields2(); + makeImmutable(); return this; } @@ -2613,6 +2611,48 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, 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 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, 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 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, this.resizeableActivity = sForBoolean.unparcel(in); this.autoRevokePermissions = in.readInt(); - this.mimeGroups = (ArraySet) 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 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, 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 eac084228b81c..28309c77f8cfb 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.PackageSetting; 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 @@ public class PackageStateImpl implements PackageState { 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 @@ public class PackageStateImpl implements PackageState { 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 @@ public class PackageStateImpl implements PackageState { 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 2c8b977b2bd68..e4eed5f84544e 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.annotation.Nullable; 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 @@ public class PackageStateUnserialized { 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 1bcc3d1f70ad8..1c6ba33bd279a 100644 --- a/services/tests/PackageManagerServiceTests/unit/Android.bp +++ b/services/tests/PackageManagerServiceTests/unit/Android.bp @@ -35,6 +35,7 @@ android_test { "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 5361041c4aff8..b41fd39b355c6 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.FeatureGroupInfo 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 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag "addMimeGroupsFromComponent", "assignDerivedFields", "assignDerivedFields2", + "makeImmutable", "buildFakeForDeletion", "buildAppClassNamesByProcess", "capPermissionPriorities", @@ -219,6 +219,7 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag AndroidPackage::isNativeLibraryRootRequiresIsa, AndroidPackage::isOdm, AndroidPackage::isOem, + AndroidPackage::isOnBackInvokedCallbackEnabled, AndroidPackage::isOverlay, AndroidPackage::isOverlayIsStatic, AndroidPackage::isPartiallyDirectBootAware, @@ -279,7 +280,7 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag adder(AndroidPackage::getUsesOptionalNativeLibraries, "testUsesOptionalNativeLibrary"), getSetByValue( AndroidPackage::areAttributionsUserVisible, - ParsingPackage::setAttributionsAreUserVisible, + PackageImpl::setAttributionsAreUserVisible, true ), getSetByValue2( @@ -513,11 +514,6 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag } ), getter(AndroidPackage::getKnownActivityEmbeddingCerts, setOf("TESTEMBEDDINGCERT")), - getSetByValue( - AndroidPackage::isOnBackInvokedCallbackEnabled, - ParsingPackage::setOnBackInvokedCallbackEnabled, - true - ) ) override fun initialObject() = PackageImpl.forParsing( @@ -560,10 +556,13 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag .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 e16a1871f8b8b..37bb9354df1c2 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 @@ abstract class ParcelableComponentTest( first?.let { property(it) } == second?.let { property(it) } } - @Test - fun valueComparison() { + fun buildBefore(): Pair, 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 @@ abstract class ParcelableComponentTest( 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 @@ abstract class ParcelableComponentTest( .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 0000000000000..7e9e433c29ac1 --- /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() + 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().apply { add("com.test.EnabledComponent") }) + setDisabledComponents(ArraySet().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, + parentChain: List, + 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, + 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