Skip to content

Commit 1cf30b9

Browse files
committed
Catch all bundletool exceptions when matching device APKs
GetAppDownloadInfo and GetAppUpdateInfo pass a fully client-controlled device spec to bundletool's ApkMatcher. Only the spec's presence is validated (the appstore-api DeviceAttributes.spec field is required, but its contents are not), so arbitrary device specs reach bundletool. getMatchingApkPaths only caught IncompatibleDeviceException, but bundletool rejects specs it cannot interpret with other sibling BundleToolException subtypes. For example, an app whose bundle has multi-ABI variant targeting causes MultiAbiMatcher to throw an InvalidCommandException for an unrecognized ABI in the device spec. Those escaped uncaught instead of being treated as an incompatible device. Catch the common BundleToolException base instead so any device spec bundletool cannot match or interpret yields an empty match set.
1 parent d0cc611 commit 1cf30b9

4 files changed

Lines changed: 131 additions & 3 deletions

File tree

directory/src/main/kotlin/app/accrescent/server/directory/ApkMatchingUtils.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import app.accrescent.appstore.v1.DeviceAttributes
88
import app.accrescent.bundletool.android.bundle.Commands
99
import app.accrescent.bundletool.android.bundle.Devices
1010
import com.android.tools.build.bundletool.device.ApkMatcher
11-
import com.android.tools.build.bundletool.model.exceptions.IncompatibleDeviceException
11+
import com.android.tools.build.bundletool.model.exceptions.BundleToolException
1212
import java.util.Optional
1313

1414
/**
@@ -17,7 +17,7 @@ import java.util.Optional
1717
* @param appMetadata the `BuildApksResult` of the app
1818
* @param deviceAttributes the device attributes of the device
1919
* @return a list of APK paths in the APK set associated with [appMetadata] and matching the given
20-
* device, or an empty list if none match
20+
* device, or an empty list if none match or bundletool rejects [deviceAttributes]
2121
*/
2222
fun getMatchingApkPaths(
2323
appMetadata: Commands.BuildApksResult,
@@ -31,7 +31,7 @@ fun getMatchingApkPaths(
3131
false,
3232
true,
3333
).getMatchingApks(appMetadata)
34-
} catch (_: IncompatibleDeviceException) {
34+
} catch (_: BundleToolException) {
3535
emptyList()
3636
}.map { it.path.toString() }
3737

directory/src/test/kotlin/app/accrescent/server/directory/AppServiceImplTest.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,32 @@ class AppServiceImplImplTest {
271271
)
272272
}
273273

274+
@Test
275+
fun getAppDownloadInfoWithUnrecognizedDeviceAbiReturnsFailedPrecondition() {
276+
KafkaHelper.publishApps(kafka, TestDataHelper.validMultiAbiAppPublicationRequested)
277+
278+
val request = getAppDownloadInfoRequest {
279+
appId = "com.example.nativeapp"
280+
deviceAttributes = deviceAttributesWithUnrecognizedAbi
281+
}
282+
283+
val status = CompletableFuture<Status.Code>()
284+
appService.getAppDownloadInfo(request)
285+
.subscribe()
286+
.with(
287+
{ status.complete(Status.Code.OK) },
288+
{
289+
require(it is StatusRuntimeException)
290+
status.complete(it.status.code)
291+
},
292+
)
293+
294+
assertEquals(
295+
Status.Code.FAILED_PRECONDITION,
296+
status.get(REQUEST_TIMEOUT_SECS, TimeUnit.SECONDS),
297+
)
298+
}
299+
274300
private fun getExpectedAppDownloadInfoResponse() = getAppDownloadInfoResponse {
275301
appDownloadInfo = appDownloadInfo {
276302
splitDownloadInfo.addAll(
@@ -307,6 +333,11 @@ class AppServiceImplImplTest {
307333
}
308334
.build()
309335

336+
private val deviceAttributesWithUnrecognizedAbi: DeviceAttributes = validDeviceAttributes
337+
.toBuilder()
338+
.setSpec(validDeviceAttributes.spec.toBuilder().addSupportedAbis("definitely-not-an-abi"))
339+
.build()
340+
310341
private val validGetAppListingRequest = getAppListingRequest {
311342
appId = "app.accrescent.client"
312343
preferredLanguages.add("en-US")

directory/src/test/kotlin/app/accrescent/server/directory/TestDataHelper.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ object TestDataHelper {
5454
}
5555
.build()
5656

57+
val validMultiAbiApp: App = TestDataHelper::class.java
58+
.classLoader
59+
.getResourceAsStream("valid-app-multi-abi.txtpb")!!
60+
.use {
61+
val builder = App.newBuilder()
62+
it.reader().use { TextFormat.merge(it, builder) }
63+
builder
64+
}
65+
.build()
66+
val validMultiAbiAppPublicationRequested: AppPublicationRequested = appPublicationRequested {
67+
app = validMultiAbiApp
68+
}
69+
5770
val invalidAppEditPublicationRequested = validAppEditPublicationRequested.copy { clearEdit() }
5871
val invalidAppPublicationRequested = validAppPublicationRequested.copy { clearApp() }
5972
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Copyright 2025 Logan Magee
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0-only
4+
5+
# proto-file: accrescent/server/events/v1/app.proto
6+
# proto-message: App
7+
8+
# A minimal app whose bundle contains a standalone variant with multi-ABI variant targeting. Unlike
9+
# the split-only apps used elsewhere, this shape causes bundletool's MultiAbiMatcher to run, which
10+
# rejects an unrecognized ABI in the device spec with an InvalidCommandException rather than an
11+
# IncompatibleDeviceException.
12+
app_id: "com.example.nativeapp"
13+
default_listing_language: "en"
14+
listings {
15+
language: "en"
16+
name: "Native App"
17+
short_description: "An app whose bundle has multi-ABI variant targeting"
18+
icon {
19+
object_id: "57297a7-6f2c-4a04-9656-497af21bf6b2"
20+
}
21+
}
22+
package_metadata {
23+
release_channel {
24+
well_known: WELL_KNOWN_STABLE
25+
}
26+
package_metadata {
27+
version_code: 1
28+
version_name: "1.0.0"
29+
build_apks_result {
30+
bundletool {
31+
version: "1.18.2"
32+
}
33+
variant {
34+
targeting {
35+
sdk_version_targeting {
36+
value {
37+
min {
38+
value: 19
39+
}
40+
}
41+
}
42+
multi_abi_targeting {
43+
value {
44+
abi {
45+
alias: ARM64_V8A
46+
}
47+
}
48+
}
49+
}
50+
apk_set {
51+
module_metadata {
52+
name: "base"
53+
targeting {
54+
}
55+
delivery_type: INSTALL_TIME
56+
module_type: FEATURE_MODULE
57+
}
58+
apk_description {
59+
targeting {
60+
multi_abi_targeting {
61+
value {
62+
abi {
63+
alias: ARM64_V8A
64+
}
65+
}
66+
}
67+
}
68+
path: "standalones/standalone-arm64_v8a.apk"
69+
standalone_apk_metadata {
70+
fused_module_name: "base"
71+
}
72+
}
73+
}
74+
}
75+
}
76+
apk_object_metadata {
77+
key: "standalones/standalone-arm64_v8a.apk"
78+
value {
79+
id: "00000000-0000-0000-0000-000000000001"
80+
uncompressed_size: 1000
81+
}
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)