From c3d4e115af465ae56905e9739adfdbd154a719ba Mon Sep 17 00:00:00 2001 From: Jihoon Kang Date: Mon, 24 Jun 2024 22:16:27 +0000 Subject: [PATCH 1/3] Collect container informations This change introduces a method to collect the information of what containers (i.e. api domain, partition, or any custom defined boundaries of interest) the module belongs to. The method is called in `ModuleBase.GenerateBuildActions`. Each container objects defines the following: - name of the container - list of "restrictions", which are the containers that a module that belongs to this container is not allowed to depend on. Each "restrictions" also defines custom rules which allow bypassing the restricted dependency. Each rules are an enum that are mapped to a function, given that functions are not hashable and thus cannot be set as a value in a provider. Note that this change is a no-op, as the container information is only collected for modules that implement the "InstallableModule" interface, which is not implemented by any other module types in this change. This will be utilized in the follow-up changes. Test: m nothing --no-skip-soong-tests Bug: 338660802 Change-Id: I9d16dfec0dcf06da464aa49ee7b23f46f1da236a --- android/Android.bp | 1 + android/container.go | 233 +++++++++++++++++++++++++++++++++++++++++++ android/image.go | 2 +- android/module.go | 2 + 4 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 android/container.go diff --git a/android/Android.bp b/android/Android.bp index ce8c9b0b3..3b17be446 100644 --- a/android/Android.bp +++ b/android/Android.bp @@ -41,6 +41,7 @@ bootstrap_go_package { "build_prop.go", "buildinfo_prop.go", "config.go", + "container.go", "test_config.go", "configurable_properties.go", "configured_jars.go", diff --git a/android/container.go b/android/container.go new file mode 100644 index 000000000..c4fdd9c91 --- /dev/null +++ b/android/container.go @@ -0,0 +1,233 @@ +// Copyright 2024 Google Inc. All rights reserved. +// +// 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 + +import ( + "reflect" + "slices" + + "github.com/google/blueprint" +) + +type StubsAvailableModule interface { + IsStubsModule() bool +} + +// Returns true if the dependency module is a stubs module +var depIsStubsModule = func(_ ModuleContext, _, dep Module) bool { + if stubsModule, ok := dep.(StubsAvailableModule); ok { + return stubsModule.IsStubsModule() + } + return false +} + +// Labels of exception functions, which are used to determine special dependencies that allow +// otherwise restricted inter-container dependencies +type exceptionHandleFuncLabel int + +const ( + checkStubs exceptionHandleFuncLabel = iota +) + +// Functions cannot be used as a value passed in providers, because functions are not +// hashable. As a workaround, the exceptionHandleFunc enum values are passed using providers, +// and the corresponding functions are called from this map. +var exceptionHandleFunctionsTable = map[exceptionHandleFuncLabel]func(ModuleContext, Module, Module) bool{ + checkStubs: depIsStubsModule, +} + +type InstallableModule interface { + EnforceApiContainerChecks() bool +} + +type restriction struct { + // container of the dependency + dependency *container + + // Error message to be emitted to the user when the dependency meets this restriction + errorMessage string + + // List of labels of allowed exception functions that allows bypassing this restriction. + // If any of the functions mapped to each labels returns true, this dependency would be + // considered allowed and an error will not be thrown. + allowedExceptions []exceptionHandleFuncLabel +} +type container struct { + // The name of the container i.e. partition, api domain + name string + + // Map of dependency restricted containers. + restricted []restriction +} + +var ( + VendorContainer = &container{ + name: VendorVariation, + restricted: nil, + } + SystemContainer = &container{ + name: "system", + restricted: []restriction{ + { + dependency: VendorContainer, + errorMessage: "Module belonging to the system partition other than HALs is " + + "not allowed to depend on the vendor partition module, in order to support " + + "independent development/update cycles and to support the Generic System " + + "Image. Try depending on HALs, VNDK or AIDL instead.", + allowedExceptions: []exceptionHandleFuncLabel{}, + }, + }, + } + ProductContainer = &container{ + name: ProductVariation, + restricted: []restriction{ + { + dependency: VendorContainer, + errorMessage: "Module belonging to the product partition is not allowed to " + + "depend on the vendor partition module, as this may lead to security " + + "vulnerabilities. Try depending on the HALs or utilize AIDL instead.", + allowedExceptions: []exceptionHandleFuncLabel{}, + }, + }, + } + ApexContainer = initializeApexContainer() + CtsContainer = &container{ + name: "cts", + restricted: []restriction{ + { + dependency: SystemContainer, + errorMessage: "CTS module should not depend on the modules belonging to the " + + "system partition, including \"framework\". Depending on the system " + + "partition may lead to disclosure of implementation details and regression " + + "due to API changes across platform versions. Try depending on the stubs instead.", + allowedExceptions: []exceptionHandleFuncLabel{checkStubs}, + }, + }, + } +) + +func initializeApexContainer() *container { + apexContainer := &container{ + name: "apex", + restricted: []restriction{ + { + dependency: SystemContainer, + errorMessage: "Module belonging to Apex(es) is not allowed to depend on the " + + "modules belonging to the system partition. Either statically depend on the " + + "module or convert the depending module to java_sdk_library and depend on " + + "the stubs.", + allowedExceptions: []exceptionHandleFuncLabel{checkStubs}, + }, + }, + } + + apexContainer.restricted = append(apexContainer.restricted, restriction{ + dependency: apexContainer, + errorMessage: "Module belonging to Apex(es) is not allowed to depend on the " + + "modules belonging to other Apex(es). Either include the depending " + + "module in the Apex or convert the depending module to java_sdk_library " + + "and depend on its stubs.", + allowedExceptions: []exceptionHandleFuncLabel{checkStubs}, + }) + + return apexContainer +} + +type ContainersInfo struct { + belongingContainers []*container + + belongingApexes []ApexInfo +} + +func (c *ContainersInfo) BelongingContainers() []*container { + return c.belongingContainers +} + +var ContainersInfoProvider = blueprint.NewProvider[ContainersInfo]() + +// Determines if the module can be installed in the system partition or not. +// Logic is identical to that of modulePartition(...) defined in paths.go +func installInSystemPartition(ctx ModuleContext) bool { + module := ctx.Module() + return !module.InstallInTestcases() && + !module.InstallInData() && + !module.InstallInRamdisk() && + !module.InstallInVendorRamdisk() && + !module.InstallInDebugRamdisk() && + !module.InstallInRecovery() && + !module.InstallInVendor() && + !module.InstallInOdm() && + !module.InstallInProduct() && + determineModuleKind(module.base(), ctx.blueprintBaseModuleContext()) == platformModule +} + +func generateContainerInfo(ctx ModuleContext) ContainersInfo { + inSystem := installInSystemPartition(ctx) + inProduct := ctx.Module().InstallInProduct() + inVendor := ctx.Module().InstallInVendor() + inCts := false + inApex := false + + if m, ok := ctx.Module().(ImageInterface); ok { + inProduct = inProduct || m.ProductVariantNeeded(ctx) + inVendor = inVendor || m.VendorVariantNeeded(ctx) + } + + props := ctx.Module().GetProperties() + for _, prop := range props { + val := reflect.ValueOf(prop).Elem() + if val.Kind() == reflect.Struct { + testSuites := val.FieldByName("Test_suites") + if testSuites.IsValid() && testSuites.Kind() == reflect.Slice && slices.Contains(testSuites.Interface().([]string), "cts") { + inCts = true + } + } + } + + var belongingApexes []ApexInfo + if apexInfo, ok := ModuleProvider(ctx, AllApexInfoProvider); ok { + belongingApexes = apexInfo.ApexInfos + inApex = true + } + + containers := []*container{} + if inSystem { + containers = append(containers, SystemContainer) + } + if inProduct { + containers = append(containers, ProductContainer) + } + if inVendor { + containers = append(containers, VendorContainer) + } + if inCts { + containers = append(containers, CtsContainer) + } + if inApex { + containers = append(containers, ApexContainer) + } + + return ContainersInfo{ + belongingContainers: containers, + belongingApexes: belongingApexes, + } +} + +func setContainerInfo(ctx ModuleContext) { + if _, ok := ctx.Module().(InstallableModule); ok { + containersInfo := generateContainerInfo(ctx) + SetProvider(ctx, ContainersInfoProvider, containersInfo) + } +} diff --git a/android/image.go b/android/image.go index c278dcdf9..0f0310701 100644 --- a/android/image.go +++ b/android/image.go @@ -22,7 +22,7 @@ type ImageInterface interface { // VendorVariantNeeded should return true if the module needs a vendor variant (installed on the vendor image). VendorVariantNeeded(ctx BaseModuleContext) bool - // ProductVariantNeeded should return true if the module needs a product variant (unstalled on the product image). + // ProductVariantNeeded should return true if the module needs a product variant (installed on the product image). ProductVariantNeeded(ctx BaseModuleContext) bool // CoreVariantNeeded should return true if the module needs a core variant (installed on the system image). diff --git a/android/module.go b/android/module.go index 2dc63d6e5..f4a188f15 100644 --- a/android/module.go +++ b/android/module.go @@ -1801,6 +1801,8 @@ func (m *ModuleBase) GenerateBuildActions(blueprintCtx blueprint.ModuleContext) variables: make(map[string]string), } + setContainerInfo(ctx) + m.licenseMetadataFile = PathForModuleOut(ctx, "meta_lic") dependencyInstallFiles, dependencyPackagingSpecs := m.computeInstallDeps(ctx) From f86fe9a87660a49888c67ea4646f2a1934c50f92 Mon Sep 17 00:00:00 2001 From: Jihoon Kang Date: Wed, 26 Jun 2024 22:18:10 +0000 Subject: [PATCH 2/3] Implement InstallableModule for Java modules This change enables the container information to be collected for Java modules. Test: m nothing --no-skip-soong-tests Bug: 338660802 Change-Id: I01bf99fa274275a608601ad6248d577ae8f6dffc --- apex/Android.bp | 1 + apex/container_test.go | 329 +++++++++++++++++++++++++++++++++++++++++ java/Android.bp | 1 + java/base.go | 12 ++ java/container_test.go | 129 ++++++++++++++++ 5 files changed, 472 insertions(+) create mode 100644 apex/container_test.go create mode 100644 java/container_test.go diff --git a/apex/Android.bp b/apex/Android.bp index abae9e261..17fdfc36a 100644 --- a/apex/Android.bp +++ b/apex/Android.bp @@ -37,6 +37,7 @@ bootstrap_go_package { "apex_test.go", "bootclasspath_fragment_test.go", "classpath_element_test.go", + "container_test.go", "dexpreopt_bootjars_test.go", "platform_bootclasspath_test.go", "systemserver_classpath_fragment_test.go", diff --git a/apex/container_test.go b/apex/container_test.go new file mode 100644 index 000000000..39311741d --- /dev/null +++ b/apex/container_test.go @@ -0,0 +1,329 @@ +// Copyright 2024 Google Inc. All rights reserved. +// +// 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 apex + +import ( + "android/soong/android" + "android/soong/java" + "fmt" + "testing" +) + +var checkContainerMatch = func(t *testing.T, name string, container string, expected bool, actual bool) { + errorMessage := fmt.Sprintf("module %s container %s value differ", name, container) + android.AssertBoolEquals(t, errorMessage, expected, actual) +} + +func TestApexDepsContainers(t *testing.T) { + result := android.GroupFixturePreparers( + prepareForApexTest, + java.PrepareForTestWithJavaSdkLibraryFiles, + java.FixtureWithLastReleaseApis("mybootclasspathlib"), + ).RunTestWithBp(t, ` + apex { + name: "myapex", + key: "myapex.key", + bootclasspath_fragments: [ + "mybootclasspathfragment", + ], + updatable: true, + min_sdk_version: "30", + } + apex_key { + name: "myapex.key", + public_key: "testkey.avbpubkey", + private_key: "testkey.pem", + } + bootclasspath_fragment { + name: "mybootclasspathfragment", + contents: [ + "mybootclasspathlib", + ], + apex_available: [ + "myapex", + ], + hidden_api: { + split_packages: ["*"], + }, + } + java_sdk_library { + name: "mybootclasspathlib", + srcs: [ + "mybootclasspathlib.java", + ], + apex_available: [ + "myapex", + ], + compile_dex: true, + static_libs: [ + "foo", + "baz", + ], + libs: [ + "bar", + ], + min_sdk_version: "30", + } + java_library { + name: "foo", + srcs:[ + "A.java", + ], + apex_available: [ + "myapex", + ], + min_sdk_version: "30", + } + java_library { + name: "bar", + srcs:[ + "A.java", + ], + min_sdk_version: "30", + } + java_library { + name: "baz", + srcs:[ + "A.java", + ], + apex_available: [ + "//apex_available:platform", + "myapex", + ], + min_sdk_version: "30", + } + `) + testcases := []struct { + moduleName string + variant string + isSystemContainer bool + isApexContainer bool + }{ + { + moduleName: "mybootclasspathlib", + variant: "android_common_myapex", + isSystemContainer: true, + isApexContainer: true, + }, + { + moduleName: "mybootclasspathlib.impl", + variant: "android_common_apex30", + isSystemContainer: true, + isApexContainer: true, + }, + { + moduleName: "mybootclasspathlib.stubs", + variant: "android_common", + isSystemContainer: true, + isApexContainer: false, + }, + { + moduleName: "foo", + variant: "android_common_apex30", + isSystemContainer: true, + isApexContainer: true, + }, + { + moduleName: "bar", + variant: "android_common", + isSystemContainer: true, + isApexContainer: false, + }, + { + moduleName: "baz", + variant: "android_common_apex30", + isSystemContainer: true, + isApexContainer: true, + }, + } + + for _, c := range testcases { + m := result.ModuleForTests(c.moduleName, c.variant) + containers, _ := android.OtherModuleProvider(result.TestContext.OtherModuleProviderAdaptor(), m.Module(), android.ContainersInfoProvider) + belongingContainers := containers.BelongingContainers() + checkContainerMatch(t, c.moduleName, "system", c.isSystemContainer, android.InList(android.SystemContainer, belongingContainers)) + checkContainerMatch(t, c.moduleName, "apex", c.isApexContainer, android.InList(android.ApexContainer, belongingContainers)) + } +} + +func TestNonUpdatableApexDepsContainers(t *testing.T) { + result := android.GroupFixturePreparers( + prepareForApexTest, + java.PrepareForTestWithJavaSdkLibraryFiles, + java.FixtureWithLastReleaseApis("mybootclasspathlib"), + ).RunTestWithBp(t, ` + apex { + name: "myapex", + key: "myapex.key", + bootclasspath_fragments: [ + "mybootclasspathfragment", + ], + updatable: false, + } + apex_key { + name: "myapex.key", + public_key: "testkey.avbpubkey", + private_key: "testkey.pem", + } + bootclasspath_fragment { + name: "mybootclasspathfragment", + contents: [ + "mybootclasspathlib", + ], + apex_available: [ + "myapex", + ], + hidden_api: { + split_packages: ["*"], + }, + } + java_sdk_library { + name: "mybootclasspathlib", + srcs: [ + "mybootclasspathlib.java", + ], + apex_available: [ + "myapex", + ], + compile_dex: true, + static_libs: [ + "foo", + ], + libs: [ + "bar", + ], + } + java_library { + name: "foo", + srcs:[ + "A.java", + ], + apex_available: [ + "myapex", + ], + } + java_library { + name: "bar", + srcs:[ + "A.java", + ], + } + `) + testcases := []struct { + moduleName string + variant string + isSystemContainer bool + isApexContainer bool + }{ + { + moduleName: "mybootclasspathlib", + variant: "android_common_myapex", + isSystemContainer: true, + isApexContainer: true, + }, + { + moduleName: "mybootclasspathlib.impl", + variant: "android_common_apex10000", + isSystemContainer: true, + isApexContainer: true, + }, + { + moduleName: "mybootclasspathlib.stubs", + variant: "android_common", + isSystemContainer: true, + isApexContainer: false, + }, + { + moduleName: "foo", + variant: "android_common_apex10000", + isSystemContainer: true, + isApexContainer: true, + }, + { + moduleName: "bar", + variant: "android_common", + isSystemContainer: true, + isApexContainer: false, + }, + } + + for _, c := range testcases { + m := result.ModuleForTests(c.moduleName, c.variant) + containers, _ := android.OtherModuleProvider(result.TestContext.OtherModuleProviderAdaptor(), m.Module(), android.ContainersInfoProvider) + belongingContainers := containers.BelongingContainers() + checkContainerMatch(t, c.moduleName, "system", c.isSystemContainer, android.InList(android.SystemContainer, belongingContainers)) + checkContainerMatch(t, c.moduleName, "apex", c.isApexContainer, android.InList(android.ApexContainer, belongingContainers)) + } +} + +func TestUpdatableAndNonUpdatableApexesIdenticalMinSdkVersion(t *testing.T) { + result := android.GroupFixturePreparers( + prepareForApexTest, + java.PrepareForTestWithJavaSdkLibraryFiles, + android.FixtureMergeMockFs(android.MockFS{ + "system/sepolicy/apex/myapex_non_updatable-file_contexts": nil, + "system/sepolicy/apex/myapex_updatable-file_contexts": nil, + }), + ).RunTestWithBp(t, ` + apex { + name: "myapex_non_updatable", + key: "myapex_non_updatable.key", + java_libs: [ + "foo", + ], + updatable: false, + min_sdk_version: "30", + } + apex_key { + name: "myapex_non_updatable.key", + public_key: "testkey.avbpubkey", + private_key: "testkey.pem", + } + + apex { + name: "myapex_updatable", + key: "myapex_updatable.key", + java_libs: [ + "foo", + ], + updatable: true, + min_sdk_version: "30", + } + apex_key { + name: "myapex_updatable.key", + public_key: "testkey.avbpubkey", + private_key: "testkey.pem", + } + + java_library { + name: "foo", + srcs:[ + "A.java", + ], + apex_available: [ + "myapex_non_updatable", + "myapex_updatable", + ], + min_sdk_version: "30", + sdk_version: "current", + } + `) + + fooApexVariant := result.ModuleForTests("foo", "android_common_apex30") + containers, _ := android.OtherModuleProvider(result.TestContext.OtherModuleProviderAdaptor(), fooApexVariant.Module(), android.ContainersInfoProvider) + belongingContainers := containers.BelongingContainers() + checkContainerMatch(t, "foo", "system", true, android.InList(android.SystemContainer, belongingContainers)) + checkContainerMatch(t, "foo", "apex", true, android.InList(android.ApexContainer, belongingContainers)) +} diff --git a/java/Android.bp b/java/Android.bp index 54b36ab60..a941754db 100644 --- a/java/Android.bp +++ b/java/Android.bp @@ -87,6 +87,7 @@ bootstrap_go_package { "app_set_test.go", "app_test.go", "code_metadata_test.go", + "container_test.go", "bootclasspath_fragment_test.go", "device_host_converter_test.go", "dex_test.go", diff --git a/java/base.go b/java/base.go index ee8df3e76..ad79e986d 100644 --- a/java/base.go +++ b/java/base.go @@ -552,6 +552,18 @@ type Module struct { aconfigCacheFiles android.Paths } +var _ android.InstallableModule = (*Module)(nil) + +// To satisfy the InstallableModule interface +func (j *Module) EnforceApiContainerChecks() bool { + return true +} + +// Overrides android.ModuleBase.InstallInProduct() +func (j *Module) InstallInProduct() bool { + return j.ProductSpecific() +} + func (j *Module) CheckStableSdkVersion(ctx android.BaseModuleContext) error { sdkVersion := j.SdkVersion(ctx) if sdkVersion.Stable() { diff --git a/java/container_test.go b/java/container_test.go new file mode 100644 index 000000000..344185553 --- /dev/null +++ b/java/container_test.go @@ -0,0 +1,129 @@ +// Copyright 2024 Google Inc. All rights reserved. +// +// 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 java + +import ( + "android/soong/android" + "fmt" + "testing" +) + +var checkContainerMatch = func(t *testing.T, name string, container string, expected bool, actual bool) { + errorMessage := fmt.Sprintf("module %s container %s value differ", name, container) + android.AssertBoolEquals(t, errorMessage, expected, actual) +} + +func TestJavaContainersModuleProperties(t *testing.T) { + result := android.GroupFixturePreparers( + prepareForJavaTest, + ).RunTestWithBp(t, ` + java_library { + name: "foo", + srcs: ["A.java"], + } + java_library { + name: "foo_vendor", + srcs: ["A.java"], + vendor: true, + sdk_version: "current", + } + java_library { + name: "foo_soc_specific", + srcs: ["A.java"], + soc_specific: true, + sdk_version: "current", + } + java_library { + name: "foo_product_specific", + srcs: ["A.java"], + product_specific: true, + sdk_version: "current", + } + java_test { + name: "foo_cts_test", + srcs: ["A.java"], + test_suites: [ + "cts", + ], + } + java_test { + name: "foo_non_cts_test", + srcs: ["A.java"], + test_suites: [ + "general-tests", + ], + } + `) + + testcases := []struct { + moduleName string + isSystemContainer bool + isVendorContainer bool + isProductContainer bool + isCts bool + }{ + { + moduleName: "foo", + isSystemContainer: true, + isVendorContainer: false, + isProductContainer: false, + isCts: false, + }, + { + moduleName: "foo_vendor", + isSystemContainer: false, + isVendorContainer: true, + isProductContainer: false, + isCts: false, + }, + { + moduleName: "foo_soc_specific", + isSystemContainer: false, + isVendorContainer: true, + isProductContainer: false, + isCts: false, + }, + { + moduleName: "foo_product_specific", + isSystemContainer: false, + isVendorContainer: false, + isProductContainer: true, + isCts: false, + }, + { + moduleName: "foo_cts_test", + isSystemContainer: false, + isVendorContainer: false, + isProductContainer: false, + isCts: true, + }, + { + moduleName: "foo_non_cts_test", + isSystemContainer: false, + isVendorContainer: false, + isProductContainer: false, + isCts: false, + }, + } + + for _, c := range testcases { + m := result.ModuleForTests(c.moduleName, "android_common") + containers, _ := android.OtherModuleProvider(result.TestContext.OtherModuleProviderAdaptor(), m.Module(), android.ContainersInfoProvider) + belongingContainers := containers.BelongingContainers() + checkContainerMatch(t, c.moduleName, "system", c.isSystemContainer, android.InList(android.SystemContainer, belongingContainers)) + checkContainerMatch(t, c.moduleName, "vendor", c.isVendorContainer, android.InList(android.VendorContainer, belongingContainers)) + checkContainerMatch(t, c.moduleName, "product", c.isProductContainer, android.InList(android.ProductContainer, belongingContainers)) + } +} From b7e1a48376e80feb5501fae0f6ec97001e111b74 Mon Sep 17 00:00:00 2001 From: Jihoon Kang Date: Wed, 26 Jun 2024 22:11:02 +0000 Subject: [PATCH 3/3] Add util method HasIntersection(...) Introduces a util method `HasIntersection(...)`, which returns true if the two input lists have non-empty intersection. Test: m nothing --no-skip-soong-tests Bug: 338660802 Change-Id: I861a60e1973c92a773c127dfc595ad7514d7972f --- android/util.go | 6 ++++++ android/util_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/android/util.go b/android/util.go index e21e66b88..3c0af2f38 100644 --- a/android/util.go +++ b/android/util.go @@ -201,6 +201,12 @@ func ListSetDifference[T comparable](l1, l2 []T) (bool, []T, []T) { return listsDiffer, diff1, diff2 } +// Returns true if the two lists have common elements. +func HasIntersection[T comparable](l1, l2 []T) bool { + _, a, b := ListSetDifference(l1, l2) + return len(a)+len(b) < len(setFromList(l1))+len(setFromList(l2)) +} + // Returns true if the given string s is prefixed with any string in the given prefix list. func HasAnyPrefix(s string, prefixList []string) bool { for _, prefix := range prefixList { diff --git a/android/util_test.go b/android/util_test.go index 8e73d835c..6537d69b9 100644 --- a/android/util_test.go +++ b/android/util_test.go @@ -818,3 +818,52 @@ func TestReverseSlice(t *testing.T) { }) } } + +var hasIntersectionTestCases = []struct { + name string + l1 []string + l2 []string + expected bool +}{ + { + name: "empty", + l1: []string{"a", "b", "c"}, + l2: []string{}, + expected: false, + }, + { + name: "both empty", + l1: []string{}, + l2: []string{}, + expected: false, + }, + { + name: "identical", + l1: []string{"a", "b", "c"}, + l2: []string{"a", "b", "c"}, + expected: true, + }, + { + name: "duplicates", + l1: []string{"a", "a", "a"}, + l2: []string{"a", "b", "c"}, + expected: true, + }, + { + name: "duplicates with no intersection", + l1: []string{"d", "d", "d", "d"}, + l2: []string{"a", "b", "c"}, + expected: false, + }, +} + +func TestHasIntersection(t *testing.T) { + for _, testCase := range hasIntersectionTestCases { + t.Run(testCase.name, func(t *testing.T) { + hasIntersection := HasIntersection(testCase.l1, testCase.l2) + if !reflect.DeepEqual(hasIntersection, testCase.expected) { + t.Errorf("expected %#v, got %#v", testCase.expected, hasIntersection) + } + }) + } +}