blob: e8762a3e935c42b0a37693e2e5c39f420a8c5b6e [file] [log] [blame]
Aurimas Liutikas88c7ff12023-08-10 12:42:26 -07001/*
2 * Copyright (C) 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.compat.overrides;
18
19import static android.content.pm.PackageManager.CERT_INPUT_SHA256;
20import static android.content.pm.PackageManager.MATCH_ANY_USER;
21
22import static java.util.Collections.emptyMap;
23import static java.util.Collections.emptySet;
24
25import android.annotation.Nullable;
26import android.app.compat.PackageOverride;
27import android.content.pm.ApplicationInfo;
28import android.content.pm.PackageManager;
29import android.util.ArrayMap;
30import android.util.ArraySet;
31import android.util.KeyValueListParser;
32import android.util.Pair;
33import android.util.Slog;
34
35import libcore.util.HexEncoding;
36
37import java.util.Arrays;
38import java.util.Comparator;
39import java.util.List;
40import java.util.Map;
41import java.util.Set;
42import java.util.regex.Pattern;
43
44/**
45 * A utility class for parsing App Compat Overrides flags.
46 *
47 * @hide
48 */
49final class AppCompatOverridesParser {
50 /**
51 * Flag for specifying all compat change IDs owned by a namespace. See {@link
52 * #parseOwnedChangeIds} for information on how this flag is parsed.
53 */
54 static final String FLAG_OWNED_CHANGE_IDS = "owned_change_ids";
55
56 /**
57 * Flag for immediately removing overrides for certain packages and change IDs (from the compat
58 * platform), as well as stopping to apply them, in case of an emergency. See {@link
59 * #parseRemoveOverrides} for information on how this flag is parsed.
60 */
61 static final String FLAG_REMOVE_OVERRIDES = "remove_overrides";
62
63 private static final String TAG = "AppCompatOverridesParser";
64
65 private static final String WILDCARD_SYMBOL = "*";
66
67 private static final Pattern BOOLEAN_PATTERN =
68 Pattern.compile("true|false", Pattern.CASE_INSENSITIVE);
69
70 private static final String WILDCARD_NO_OWNED_CHANGE_IDS_WARNING =
71 "Wildcard can't be used in '" + FLAG_REMOVE_OVERRIDES + "' flag with an empty "
72 + FLAG_OWNED_CHANGE_IDS + "' flag";
73
74 private final PackageManager mPackageManager;
75
76 AppCompatOverridesParser(PackageManager packageManager) {
77 mPackageManager = packageManager;
78 }
79
80 /**
81 * Parses the given {@code configStr} and returns a map from package name to a set of change
82 * IDs to remove for that package.
83 *
84 * <p>The given {@code configStr} is expected to either be:
85 *
86 * <ul>
87 * <li>'*' (wildcard), to indicate that all owned overrides, specified in {@code
88 * ownedChangeIds}, for all installed packages should be removed.
89 * <li>A comma separated key value list, where the key is a package name and the value is
90 * either:
91 * <ul>
92 * <li>'*' (wildcard), to indicate that all owned overrides, specified in {@code
93 * ownedChangeIds} for that package should be removed.
94 * <li>A colon separated list of change IDs to remove for that package.
95 * </ul>
96 * </ul>
97 *
98 * <p>If the given {@code configStr} doesn't match the expected format, an empty map will be
99 * returned. If a specific change ID isn't a valid long, it will be ignored.
100 */
101 Map<String, Set<Long>> parseRemoveOverrides(String configStr, Set<Long> ownedChangeIds) {
102 if (configStr.isEmpty()) {
103 return emptyMap();
104 }
105
106 Map<String, Set<Long>> result = new ArrayMap<>();
107 if (configStr.equals(WILDCARD_SYMBOL)) {
108 if (ownedChangeIds.isEmpty()) {
109 Slog.w(TAG, WILDCARD_NO_OWNED_CHANGE_IDS_WARNING);
110 return emptyMap();
111 }
112 List<ApplicationInfo> installedApps = mPackageManager.getInstalledApplications(
113 MATCH_ANY_USER);
114 for (ApplicationInfo appInfo : installedApps) {
115 result.put(appInfo.packageName, ownedChangeIds);
116 }
117 return result;
118 }
119
120 KeyValueListParser parser = new KeyValueListParser(',');
121 try {
122 parser.setString(configStr);
123 } catch (IllegalArgumentException e) {
124 Slog.w(
125 TAG,
126 "Invalid format in '" + FLAG_REMOVE_OVERRIDES + "' flag: " + configStr, e);
127 return emptyMap();
128 }
129 for (int i = 0; i < parser.size(); i++) {
130 String packageName = parser.keyAt(i);
131 String changeIdsStr = parser.getString(packageName, /* def= */ "");
132 if (changeIdsStr.equals(WILDCARD_SYMBOL)) {
133 if (ownedChangeIds.isEmpty()) {
134 Slog.w(TAG, WILDCARD_NO_OWNED_CHANGE_IDS_WARNING);
135 continue;
136 }
137 result.put(packageName, ownedChangeIds);
138 } else {
139 for (String changeIdStr : changeIdsStr.split(":")) {
140 try {
141 long changeId = Long.parseLong(changeIdStr);
142 result.computeIfAbsent(packageName, k -> new ArraySet<>()).add(changeId);
143 } catch (NumberFormatException e) {
144 Slog.w(
145 TAG,
146 "Invalid change ID in '" + FLAG_REMOVE_OVERRIDES + "' flag: "
147 + changeIdStr, e);
148 }
149 }
150 }
151 }
152
153 return result;
154 }
155
156
157 /**
158 * Parses the given {@code configStr}, that is expected to be a comma separated list of change
159 * IDs, into a set.
160 *
161 * <p>If any of the change IDs isn't a valid long, it will be ignored.
162 */
163 static Set<Long> parseOwnedChangeIds(String configStr) {
164 if (configStr.isEmpty()) {
165 return emptySet();
166 }
167
168 Set<Long> result = new ArraySet<>();
169 for (String changeIdStr : configStr.split(",")) {
170 try {
171 result.add(Long.parseLong(changeIdStr));
172 } catch (NumberFormatException e) {
173 Slog.w(TAG,
174 "Invalid change ID in '" + FLAG_OWNED_CHANGE_IDS + "' flag: " + changeIdStr,
175 e);
176 }
177 }
178 return result;
179 }
180
181 /**
182 * Parses the given {@code configStr}, that is expected to be a comma separated list of changes
183 * overrides, and returns a map from change ID to {@link PackageOverride} instances to add.
184 *
185 * <p>Each change override is in the following format:
186 * '<signature?>~<change-id>:<min-version-code?>:<max-version-code?>:<enabled>'.
187 *
188 * <p>The signature is optional, and will only be enforced if included.
189 *
190 * <p>If there are multiple overrides that should be added with the same change ID, the one
191 * that best fits the given {@code versionCode} is added.
192 *
193 * <p>Any overrides whose change ID is in {@code changeIdsToSkip} are ignored.
194 *
195 * <p>If a change override entry in {@code configStr} is invalid, it will be ignored.
196 */
197 Map<Long, PackageOverride> parsePackageOverrides(String configStr, String packageName,
198 long versionCode,
199 Set<Long> changeIdsToSkip) {
200 if (configStr.isEmpty()) {
201 return emptyMap();
202 }
203 PackageOverrideComparator comparator = new PackageOverrideComparator(versionCode);
204 Map<Long, PackageOverride> overridesToAdd = new ArrayMap<>();
205
206 Pair<String, String> signatureAndConfig = extractSignatureFromConfig(configStr);
207 if (signatureAndConfig == null) {
208 return emptyMap();
209 }
210 final String signature = signatureAndConfig.first;
211 final String overridesConfig = signatureAndConfig.second;
212
213 if (!verifySignature(packageName, signature)) {
214 return emptyMap();
215 }
216
217 for (String overrideEntryString : overridesConfig.split(",")) {
218 List<String> changeIdAndVersions = Arrays.asList(overrideEntryString.split(":", 4));
219 if (changeIdAndVersions.size() != 4) {
220 Slog.w(TAG, "Invalid change override entry: " + overrideEntryString);
221 continue;
222 }
223 long changeId;
224 try {
225 changeId = Long.parseLong(changeIdAndVersions.get(0));
226 } catch (NumberFormatException e) {
227 Slog.w(TAG, "Invalid change ID in override entry: " + overrideEntryString, e);
228 continue;
229 }
230
231 if (changeIdsToSkip.contains(changeId)) {
232 continue;
233 }
234
235 String minVersionCodeStr = changeIdAndVersions.get(1);
236 String maxVersionCodeStr = changeIdAndVersions.get(2);
237
238 String enabledStr = changeIdAndVersions.get(3);
239 if (!BOOLEAN_PATTERN.matcher(enabledStr).matches()) {
240 Slog.w(TAG, "Invalid enabled string in override entry: " + overrideEntryString);
241 continue;
242 }
243 boolean enabled = Boolean.parseBoolean(enabledStr);
244 PackageOverride.Builder overrideBuilder = new PackageOverride.Builder().setEnabled(
245 enabled);
246 try {
247 if (!minVersionCodeStr.isEmpty()) {
248 overrideBuilder.setMinVersionCode(Long.parseLong(minVersionCodeStr));
249 }
250 if (!maxVersionCodeStr.isEmpty()) {
251 overrideBuilder.setMaxVersionCode(Long.parseLong(maxVersionCodeStr));
252 }
253 } catch (NumberFormatException e) {
254 Slog.w(TAG,
255 "Invalid min/max version code in override entry: " + overrideEntryString,
256 e);
257 continue;
258 }
259
260 try {
261 PackageOverride override = overrideBuilder.build();
262 if (!overridesToAdd.containsKey(changeId)
263 || comparator.compare(override, overridesToAdd.get(changeId)) < 0) {
264 overridesToAdd.put(changeId, override);
265 }
266 } catch (IllegalArgumentException e) {
267 Slog.w(TAG, "Failed to build PackageOverride", e);
268 }
269 }
270
271 return overridesToAdd;
272 }
273
274 /**
275 * Extracts the signature from the config string if one exists.
276 *
277 * @param configStr String in the form of <signature?>~<overrideConfig>
278 */
279 @Nullable
280 private static Pair<String, String> extractSignatureFromConfig(String configStr) {
281 final List<String> signatureAndConfig = Arrays.asList(configStr.split("~"));
282
283 if (signatureAndConfig.size() == 1) {
284 // The config string doesn't contain a signature.
285 return Pair.create("", configStr);
286 }
287
288 if (signatureAndConfig.size() > 2) {
289 Slog.w(TAG, "Only one signature per config is supported. Config: " + configStr);
290 return null;
291 }
292
293 return Pair.create(signatureAndConfig.get(0), signatureAndConfig.get(1));
294 }
295
296 /**
297 * Verifies that the specified package was signed with a particular signature.
298 *
299 * @param packageName The package to check.
300 * @param signature The optional signature to verify. If empty, we return true.
301 * @return Whether the package is signed with that signature.
302 */
303 private boolean verifySignature(String packageName, String signature) {
304 try {
305 final boolean signatureValid = signature.isEmpty()
306 || mPackageManager.hasSigningCertificate(packageName,
307 HexEncoding.decode(signature), CERT_INPUT_SHA256);
308
309 if (!signatureValid) {
310 Slog.w(TAG, packageName + " did not have expected signature: " + signature);
311 }
312 return signatureValid;
313 } catch (IllegalArgumentException e) {
314 Slog.w(TAG, "Unable to verify signature " + signature + " for " + packageName, e);
315 return false;
316 }
317 }
318
319 /**
320 * A {@link Comparator} that compares @link PackageOverride} instances with respect to a
321 * specified {@code versionCode} as follows:
322 *
323 * <ul>
324 * <li>Prefer the {@link PackageOverride} whose version range contains {@code versionCode}.
325 * <li>Otherwise, prefer the {@link PackageOverride} whose version range is closest to {@code
326 * versionCode} from below.
327 * <li>Otherwise, prefer the {@link PackageOverride} whose version range is closest to {@code
328 * versionCode} from above.
329 * </ul>
330 */
331 private static final class PackageOverrideComparator implements Comparator<PackageOverride> {
332 private final long mVersionCode;
333
334 PackageOverrideComparator(long versionCode) {
335 this.mVersionCode = versionCode;
336 }
337
338 @Override
339 public int compare(PackageOverride o1, PackageOverride o2) {
340 // Prefer overrides whose version range contains versionCode.
341 boolean isVersionInRange1 = isVersionInRange(o1, mVersionCode);
342 boolean isVersionInRange2 = isVersionInRange(o2, mVersionCode);
343 if (isVersionInRange1 != isVersionInRange2) {
344 return isVersionInRange1 ? -1 : 1;
345 }
346
347 // Otherwise, prefer overrides whose version range is before versionCode.
348 boolean isVersionAfterRange1 = isVersionAfterRange(o1, mVersionCode);
349 boolean isVersionAfterRange2 = isVersionAfterRange(o2, mVersionCode);
350 if (isVersionAfterRange1 != isVersionAfterRange2) {
351 return isVersionAfterRange1 ? -1 : 1;
352 }
353
354 // If both overrides' version ranges are either before or after versionCode, prefer
355 // those whose version range is closer to versionCode.
356 return Long.compare(
357 getVersionProximity(o1, mVersionCode), getVersionProximity(o2, mVersionCode));
358 }
359
360 /**
361 * Returns true if the version range in the given {@code override} contains {@code
362 * versionCode}.
363 */
364 private static boolean isVersionInRange(PackageOverride override, long versionCode) {
365 return override.getMinVersionCode() <= versionCode
366 && versionCode <= override.getMaxVersionCode();
367 }
368
369 /**
370 * Returns true if the given {@code versionCode} is strictly after the version range in the
371 * given {@code override}.
372 */
373 private static boolean isVersionAfterRange(PackageOverride override, long versionCode) {
374 return override.getMaxVersionCode() < versionCode;
375 }
376
377 /**
378 * Returns true if the given {@code versionCode} is strictly before the version range in the
379 * given {@code override}.
380 */
381 private static boolean isVersionBeforeRange(PackageOverride override, long versionCode) {
382 return override.getMinVersionCode() > versionCode;
383 }
384
385 /**
386 * In case the given {@code versionCode} is strictly before or after the version range in
387 * the given {@code override}, returns the distance from it, otherwise returns zero.
388 */
389 private static long getVersionProximity(PackageOverride override, long versionCode) {
390 if (isVersionAfterRange(override, versionCode)) {
391 return versionCode - override.getMaxVersionCode();
392 }
393 if (isVersionBeforeRange(override, versionCode)) {
394 return override.getMinVersionCode() - versionCode;
395 }
396
397 // Version is in range. Note that when two overrides have a zero version proximity
398 // they will be ordered arbitrarily.
399 return 0;
400 }
401 }
402}