1 /* 2 * Copyright (C) 2020 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 17 package com.android.internal.content.om; 18 19 import static com.android.internal.content.om.OverlayConfig.TAG; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.content.pm.PackagePartitions; 24 import android.content.pm.PackagePartitions.SystemPartition; 25 import android.os.Build; 26 import android.os.FileUtils; 27 import android.os.SystemProperties; 28 import android.text.TextUtils; 29 import android.util.ArraySet; 30 import android.util.Log; 31 import android.util.Xml; 32 33 import com.android.internal.annotations.VisibleForTesting; 34 import com.android.internal.content.om.OverlayScanner.ParsedOverlayInfo; 35 import com.android.internal.util.Preconditions; 36 import com.android.internal.util.XmlUtils; 37 38 import libcore.io.IoUtils; 39 40 import org.xmlpull.v1.XmlPullParser; 41 import org.xmlpull.v1.XmlPullParserException; 42 43 import java.io.File; 44 import java.io.FileNotFoundException; 45 import java.io.FileReader; 46 import java.io.IOException; 47 import java.util.ArrayList; 48 import java.util.List; 49 import java.util.Map; 50 51 /** 52 * Responsible for parsing configurations of Runtime Resource Overlays that control mutability, 53 * default enable state, and priority. To configure an overlay, create or modify the file located 54 * at {@code partition}/overlay/config/config.xml where {@code partition} is the partition of the 55 * overlay to be configured. In order to be configured, an overlay must reside in the overlay 56 * directory of the partition in which the overlay is configured. 57 * 58 * @see #parseOverlay(File, XmlPullParser, OverlayScanner, ParsingContext) 59 * @see #parseMerge(File, XmlPullParser, OverlayScanner, ParsingContext) 60 * 61 * @hide 62 **/ 63 @VisibleForTesting 64 public final class OverlayConfigParser { 65 66 /** Represents a part of a parsed overlay configuration XML file. */ 67 public static class ParsedConfigFile { 68 @NonNull public final String path; 69 @NonNull public final int line; 70 @Nullable public final String xml; 71 ParsedConfigFile(@onNull String path, int line, @Nullable String xml)72 ParsedConfigFile(@NonNull String path, int line, @Nullable String xml) { 73 this.path = path; 74 this.line = line; 75 this.xml = xml; 76 } 77 78 @Override toString()79 public String toString() { 80 StringBuilder sb = new StringBuilder(getClass().getSimpleName()); 81 sb.append("{path="); 82 sb.append(path); 83 sb.append(", line="); 84 sb.append(line); 85 if (xml != null) { 86 sb.append(", xml="); 87 sb.append(xml); 88 } 89 sb.append("}"); 90 return sb.toString(); 91 } 92 } 93 94 // Default values for overlay configurations. 95 static final boolean DEFAULT_ENABLED_STATE = false; 96 static final boolean DEFAULT_MUTABILITY = true; 97 98 // Maximum recursive depth of processing merge tags. 99 private static final int MAXIMUM_MERGE_DEPTH = 5; 100 101 // The subdirectory within a partition's overlay directory that contains the configuration files 102 // for the partition. 103 private static final String CONFIG_DIRECTORY = "config"; 104 105 /** 106 * The name of the configuration file to parse for overlay configurations. This class does not 107 * scan for overlay configuration files within the {@link #CONFIG_DIRECTORY}; rather, other 108 * files can be included at a particular position within this file using the <merge> tag. 109 * 110 * @see #parseMerge(File, XmlPullParser, OverlayScanner, ParsingContext) 111 */ 112 private static final String CONFIG_DEFAULT_FILENAME = CONFIG_DIRECTORY + "/config.xml"; 113 114 /** Represents the configurations of a particular overlay. */ 115 public static class ParsedConfiguration { 116 @NonNull 117 public final String packageName; 118 119 /** Whether or not the overlay is enabled by default. */ 120 public final boolean enabled; 121 122 /** 123 * Whether or not the overlay is mutable and can have its enabled state changed dynamically 124 * using the {@code OverlayManagerService}. 125 **/ 126 public final boolean mutable; 127 128 /** The policy granted to overlays on the partition in which the overlay is located. */ 129 @NonNull 130 public final String policy; 131 132 /** 133 * Information extracted from the manifest of the overlay. 134 * Null if the information was read from a config file instead of a manifest. 135 * 136 * @see parsedConfigFile 137 **/ 138 @Nullable 139 public final ParsedOverlayInfo parsedInfo; 140 141 /** 142 * The config file used to configure this overlay. 143 * Null if no config file was used, in which case the overlay's manifest was used instead. 144 * 145 * @see parsedInfo 146 **/ 147 @Nullable 148 public final ParsedConfigFile parsedConfigFile; 149 ParsedConfiguration(@onNull String packageName, boolean enabled, boolean mutable, @NonNull String policy, @Nullable ParsedOverlayInfo parsedInfo, @Nullable ParsedConfigFile parsedConfigFile)150 ParsedConfiguration(@NonNull String packageName, boolean enabled, boolean mutable, 151 @NonNull String policy, @Nullable ParsedOverlayInfo parsedInfo, 152 @Nullable ParsedConfigFile parsedConfigFile) { 153 this.packageName = packageName; 154 this.enabled = enabled; 155 this.mutable = mutable; 156 this.policy = policy; 157 this.parsedInfo = parsedInfo; 158 this.parsedConfigFile = parsedConfigFile; 159 } 160 161 @Override toString()162 public String toString() { 163 return getClass().getSimpleName() + String.format("{packageName=%s, enabled=%s" 164 + ", mutable=%s, policy=%s, parsedInfo=%s, parsedConfigFile=%s}", 165 packageName, enabled, mutable, policy, parsedInfo, parsedConfigFile); 166 } 167 } 168 169 /** 170 * @hide 171 **/ 172 @VisibleForTesting 173 public static class OverlayPartition extends SystemPartition { 174 // Policies passed to idmap2 during idmap creation. 175 // Keep partition policy constants in sync with f/b/cmds/idmap2/include/idmap2/Policies.h. 176 static final String POLICY_ODM = "odm"; 177 static final String POLICY_OEM = "oem"; 178 static final String POLICY_PRODUCT = "product"; 179 static final String POLICY_PUBLIC = "public"; 180 static final String POLICY_SYSTEM = "system"; 181 static final String POLICY_VENDOR = "vendor"; 182 183 @NonNull 184 public final String policy; 185 186 /** 187 * @hide 188 **/ 189 @VisibleForTesting OverlayPartition(@onNull SystemPartition partition)190 public OverlayPartition(@NonNull SystemPartition partition) { 191 super(partition); 192 this.policy = policyForPartition(partition); 193 } 194 195 /** 196 * Creates a partition containing the same folders as the original partition but with a 197 * different root folder. 198 */ OverlayPartition(@onNull File folder, @NonNull SystemPartition original)199 OverlayPartition(@NonNull File folder, @NonNull SystemPartition original) { 200 super(folder, original); 201 this.policy = policyForPartition(original); 202 } 203 policyForPartition(SystemPartition partition)204 private static String policyForPartition(SystemPartition partition) { 205 switch (partition.type) { 206 case PackagePartitions.PARTITION_SYSTEM: 207 case PackagePartitions.PARTITION_SYSTEM_EXT: 208 return POLICY_SYSTEM; 209 case PackagePartitions.PARTITION_VENDOR: 210 return POLICY_VENDOR; 211 case PackagePartitions.PARTITION_ODM: 212 return POLICY_ODM; 213 case PackagePartitions.PARTITION_OEM: 214 return POLICY_OEM; 215 case PackagePartitions.PARTITION_PRODUCT: 216 return POLICY_PRODUCT; 217 default: 218 throw new IllegalStateException("Unable to determine policy for " 219 + partition.getFolder()); 220 } 221 } 222 } 223 224 /** This class holds state related to parsing the configurations of a partition. */ 225 private static class ParsingContext { 226 // The overlay directory of the partition 227 private final OverlayPartition mPartition; 228 229 // The ordered list of configured overlays 230 private final ArrayList<ParsedConfiguration> mOrderedConfigurations = new ArrayList<>(); 231 232 // The packages configured in the partition 233 private final ArraySet<String> mConfiguredOverlays = new ArraySet<>(); 234 235 // Whether an mutable overlay has been configured in the partition 236 private boolean mFoundMutableOverlay; 237 238 // The current recursive depth of merging configuration files 239 private int mMergeDepth; 240 ParsingContext(OverlayPartition partition)241 private ParsingContext(OverlayPartition partition) { 242 mPartition = partition; 243 } 244 } 245 246 @FunctionalInterface 247 public interface SysPropWrapper{ 248 /** 249 * Get system property 250 * 251 * @param property the key to look up. 252 * 253 * @return The property value if found, empty string otherwise. 254 */ get(String property)255 String get(String property); 256 } 257 258 /** 259 * Retrieves overlays configured within the partition in increasing priority order. 260 * 261 * If {@code scanner} is null, then the {@link ParsedConfiguration#parsedInfo} fields of the 262 * added configured overlays will be null and the parsing logic will not assert that the 263 * configured overlays exist within the partition. 264 * 265 * @return list of configured overlays if configuration file exists; otherwise, null 266 */ 267 @Nullable getConfigurations( @onNull OverlayPartition partition, @Nullable OverlayScanner scanner, @Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos, @NonNull List<String> activeApexes)268 static ArrayList<ParsedConfiguration> getConfigurations( 269 @NonNull OverlayPartition partition, @Nullable OverlayScanner scanner, 270 @Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos, 271 @NonNull List<String> activeApexes) { 272 if (scanner != null) { 273 if (partition.getOverlayFolder() != null) { 274 scanner.scanDir(partition.getOverlayFolder()); 275 } 276 for (String apex : activeApexes) { 277 scanner.scanDir(new File("/apex/" + apex + "/overlay/")); 278 } 279 } 280 281 if (partition.getOverlayFolder() == null) { 282 return null; 283 } 284 285 final File configFile = new File(partition.getOverlayFolder(), CONFIG_DEFAULT_FILENAME); 286 if (!configFile.exists()) { 287 return null; 288 } 289 290 final ParsingContext parsingContext = new ParsingContext(partition); 291 readConfigFile(configFile, scanner, packageManagerOverlayInfos, parsingContext); 292 return parsingContext.mOrderedConfigurations; 293 } 294 readConfigFile(@onNull File configFile, @Nullable OverlayScanner scanner, @Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos, @NonNull ParsingContext parsingContext)295 private static void readConfigFile(@NonNull File configFile, @Nullable OverlayScanner scanner, 296 @Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos, 297 @NonNull ParsingContext parsingContext) { 298 FileReader configReader; 299 try { 300 configReader = new FileReader(configFile); 301 } catch (FileNotFoundException e) { 302 Log.w(TAG, "Couldn't find or open overlay configuration file " + configFile); 303 return; 304 } 305 306 try { 307 final XmlPullParser parser = Xml.newPullParser(); 308 parser.setInput(configReader); 309 XmlUtils.beginDocument(parser, "config"); 310 311 int depth = parser.getDepth(); 312 while (XmlUtils.nextElementWithin(parser, depth)) { 313 final String name = parser.getName(); 314 switch (name) { 315 case "merge": 316 parseMerge(configFile, parser, scanner, packageManagerOverlayInfos, 317 parsingContext); 318 break; 319 case "overlay": 320 parseOverlay(configFile, parser, scanner, packageManagerOverlayInfos, 321 parsingContext); 322 break; 323 default: 324 Log.w(TAG, String.format("Tag %s is unknown in %s at %s", 325 name, configFile, parser.getPositionDescription())); 326 break; 327 } 328 } 329 } catch (XmlPullParserException | IOException e) { 330 Log.w(TAG, "Got exception parsing overlay configuration.", e); 331 } finally { 332 IoUtils.closeQuietly(configReader); 333 } 334 } 335 336 /** 337 * Expand the property inside a rro configuration path. 338 * 339 * A RRO configuration can contain a property, this method expands 340 * the property to its value. 341 * 342 * Only read only properties allowed, prefixed with ro. Other 343 * properties will raise exception. 344 * 345 * Only a single property in the path is allowed. 346 * 347 * Example "${ro.boot.hardware.sku}/config.xml" would expand to 348 * "G020N/config.xml" 349 * 350 * @param configPath path to expand 351 * @param sysPropWrapper method used for reading properties 352 * 353 * @return The expanded path. Returns null if configPath is null. 354 */ 355 @VisibleForTesting expandProperty(String configPath, SysPropWrapper sysPropWrapper)356 public static String expandProperty(String configPath, 357 SysPropWrapper sysPropWrapper) { 358 if (configPath == null) { 359 return null; 360 } 361 362 int propStartPos = configPath.indexOf("${"); 363 if (propStartPos == -1) { 364 // No properties inside the string, return as is 365 return configPath; 366 } 367 368 final StringBuilder sb = new StringBuilder(); 369 sb.append(configPath.substring(0, propStartPos)); 370 371 // Read out the end position 372 int propEndPos = configPath.indexOf("}", propStartPos); 373 if (propEndPos == -1) { 374 throw new IllegalStateException("Malformed property, unmatched braces, in: " 375 + configPath); 376 } 377 378 // Confirm that there is only one property inside the string 379 if (configPath.indexOf("${", propStartPos + 2) != -1) { 380 throw new IllegalStateException("Only a single property supported in path: " 381 + configPath); 382 } 383 384 final String propertyName = configPath.substring(propStartPos + 2, propEndPos); 385 if (!propertyName.startsWith("ro.")) { 386 throw new IllegalStateException("Only read only properties can be used when " 387 + "merging RRO config files: " + propertyName); 388 } 389 final String propertyValue = sysPropWrapper.get(propertyName); 390 if (TextUtils.isEmpty(propertyValue)) { 391 throw new IllegalStateException("Property is empty or doesn't exist: " + propertyName); 392 } 393 Log.d(TAG, String.format("Using property in overlay config path: \"%s\"", propertyName)); 394 sb.append(propertyValue); 395 396 // propEndPos points to '}', need to step to next character, might be outside of string 397 propEndPos = propEndPos + 1; 398 // Append the remainder, if exists 399 if (propEndPos < configPath.length()) { 400 sb.append(configPath.substring(propEndPos)); 401 } 402 403 return sb.toString(); 404 } 405 406 /** 407 * Parses a <merge> tag within an overlay configuration file. 408 * 409 * Merge tags allow for other configuration files to be "merged" at the current parsing 410 * position into the current configuration file being parsed. The {@code path} attribute of the 411 * tag represents the path of the file to merge relative to the directory containing overlay 412 * configuration files. 413 */ parseMerge(@onNull File configFile, @NonNull XmlPullParser parser, @Nullable OverlayScanner scanner, @Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos, @NonNull ParsingContext parsingContext)414 private static void parseMerge(@NonNull File configFile, @NonNull XmlPullParser parser, 415 @Nullable OverlayScanner scanner, 416 @Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos, 417 @NonNull ParsingContext parsingContext) { 418 final String path; 419 420 try { 421 SysPropWrapper sysPropWrapper = p -> { 422 return SystemProperties.get(p, ""); 423 }; 424 path = expandProperty(parser.getAttributeValue(null, "path"), sysPropWrapper); 425 } catch (IllegalStateException e) { 426 throw new IllegalStateException(String.format("<merge> path expand error in %s at %s", 427 configFile, parser.getPositionDescription()), e); 428 } 429 430 if (path == null) { 431 throw new IllegalStateException(String.format("<merge> without path in %s at %s", 432 configFile, parser.getPositionDescription())); 433 } 434 435 if (path.startsWith("/")) { 436 throw new IllegalStateException(String.format( 437 "Path %s must be relative to the directory containing overlay configurations " 438 + " files in %s at %s ", path, configFile, 439 parser.getPositionDescription())); 440 } 441 442 if (parsingContext.mMergeDepth++ == MAXIMUM_MERGE_DEPTH) { 443 throw new IllegalStateException(String.format( 444 "Maximum <merge> depth exceeded in %s at %s", configFile, 445 parser.getPositionDescription())); 446 } 447 448 final File configDirectory; 449 final File includedConfigFile; 450 try { 451 configDirectory = new File(parsingContext.mPartition.getOverlayFolder(), 452 CONFIG_DIRECTORY).getCanonicalFile(); 453 includedConfigFile = new File(configDirectory, path).getCanonicalFile(); 454 } catch (IOException e) { 455 throw new IllegalStateException( 456 String.format("Couldn't find or open merged configuration file %s in %s at %s", 457 path, configFile, parser.getPositionDescription()), e); 458 } 459 460 if (!includedConfigFile.exists()) { 461 throw new IllegalStateException( 462 String.format("Merged configuration file %s does not exist in %s at %s", 463 path, configFile, parser.getPositionDescription())); 464 } 465 466 if (!FileUtils.contains(configDirectory, includedConfigFile)) { 467 throw new IllegalStateException( 468 String.format( 469 "Merged file %s outside of configuration directory in %s at %s", 470 includedConfigFile.getAbsolutePath(), includedConfigFile, 471 parser.getPositionDescription())); 472 } 473 474 readConfigFile(includedConfigFile, scanner, packageManagerOverlayInfos, parsingContext); 475 parsingContext.mMergeDepth--; 476 } 477 478 /** 479 * Parses an <overlay> tag within an overlay configuration file. 480 * 481 * Requires a {@code package} attribute that indicates which package is being configured. 482 * The optional {@code enabled} attribute controls whether or not the overlay is enabled by 483 * default (default is false). The optional {@code mutable} attribute controls whether or 484 * not the overlay is mutable and can have its enabled state changed at runtime (default is 485 * true). 486 * 487 * The order in which overlays that override the same resources are configured matters. An 488 * overlay will have a greater priority than overlays with configurations preceding its own 489 * configuration. 490 * 491 * Configurations of immutable overlays must precede configurations of mutable overlays. 492 * An overlay cannot be configured in multiple locations. All configured overlay must exist 493 * within the partition of the configuration file. An overlay cannot be configured multiple 494 * times in a single partition. 495 * 496 * Overlays not listed within a configuration file will be mutable and disabled by default. The 497 * order of non-configured overlays when enabled by the OverlayManagerService is undefined. 498 */ parseOverlay(@onNull File configFile, @NonNull XmlPullParser parser, @Nullable OverlayScanner scanner, @Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos, @NonNull ParsingContext parsingContext)499 private static void parseOverlay(@NonNull File configFile, @NonNull XmlPullParser parser, 500 @Nullable OverlayScanner scanner, 501 @Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos, 502 @NonNull ParsingContext parsingContext) { 503 Preconditions.checkArgument((scanner == null) != (packageManagerOverlayInfos == null), 504 "scanner and packageManagerOverlayInfos cannot be both null or both non-null"); 505 506 final String packageName = parser.getAttributeValue(null, "package"); 507 if (packageName == null) { 508 throw new IllegalStateException(String.format("\"<overlay> without package in %s at %s", 509 configFile, parser.getPositionDescription())); 510 } 511 512 // Ensure the overlay being configured is present in the partition during zygote 513 // initialization, unless the package is an excluded overlay package. 514 ParsedOverlayInfo info = null; 515 if (scanner != null) { 516 info = scanner.getParsedInfo(packageName); 517 if (info == null 518 && scanner.isExcludedOverlayPackage(packageName, parsingContext.mPartition)) { 519 Log.d(TAG, "overlay " + packageName + " in partition " 520 + parsingContext.mPartition.getOverlayFolder() + " is ignored."); 521 return; 522 } else if (info == null || !parsingContext.mPartition.containsOverlay(info.path)) { 523 throw new IllegalStateException( 524 String.format("overlay %s not present in partition %s in %s at %s", 525 packageName, parsingContext.mPartition.getOverlayFolder(), 526 configFile, parser.getPositionDescription())); 527 } 528 } else { 529 // Zygote shall have crashed itself, if there's an overlay apk not present in the 530 // partition. For the overlay package not found in the package manager, we can assume 531 // that it's an excluded overlay package. 532 if (packageManagerOverlayInfos.get(packageName) == null) { 533 Log.d(TAG, "overlay " + packageName + " in partition " 534 + parsingContext.mPartition.getOverlayFolder() + " is ignored."); 535 return; 536 } 537 } 538 539 if (parsingContext.mConfiguredOverlays.contains(packageName)) { 540 throw new IllegalStateException( 541 String.format("overlay %s configured multiple times in a single partition" 542 + " in %s at %s", packageName, configFile, 543 parser.getPositionDescription())); 544 } 545 546 boolean isEnabled = DEFAULT_ENABLED_STATE; 547 final String enabled = parser.getAttributeValue(null, "enabled"); 548 if (enabled != null) { 549 isEnabled = !"false".equals(enabled); 550 } 551 552 boolean isMutable = DEFAULT_MUTABILITY; 553 final String mutable = parser.getAttributeValue(null, "mutable"); 554 if (mutable != null) { 555 isMutable = !"false".equals(mutable); 556 if (!isMutable && parsingContext.mFoundMutableOverlay) { 557 throw new IllegalStateException(String.format( 558 "immutable overlays must precede mutable overlays:" 559 + " found in %s at %s", 560 configFile, parser.getPositionDescription())); 561 } 562 } 563 564 if (isMutable) { 565 parsingContext.mFoundMutableOverlay = true; 566 } else if (!isEnabled) { 567 // Default disabled, immutable overlays may be a misconfiguration of the system so warn 568 // developers. 569 Log.w(TAG, "found default-disabled immutable overlay " + packageName); 570 } 571 572 final ParsedConfigFile parsedConfigFile = new ParsedConfigFile( 573 configFile.getPath().intern(), parser.getLineNumber(), 574 (Build.IS_ENG || Build.IS_USERDEBUG) ? currentParserContextToString(parser) : null); 575 final ParsedConfiguration config = new ParsedConfiguration(packageName, isEnabled, 576 isMutable, parsingContext.mPartition.policy, info, parsedConfigFile); 577 parsingContext.mConfiguredOverlays.add(packageName); 578 parsingContext.mOrderedConfigurations.add(config); 579 } 580 currentParserContextToString(@onNull XmlPullParser parser)581 private static String currentParserContextToString(@NonNull XmlPullParser parser) { 582 StringBuilder sb = new StringBuilder("<"); 583 sb.append(parser.getName()); 584 sb.append(" "); 585 for (int i = 0; i < parser.getAttributeCount(); i++) { 586 sb.append(parser.getAttributeName(i)); 587 sb.append("=\""); 588 sb.append(parser.getAttributeValue(i)); 589 sb.append("\" "); 590 } 591 sb.append("/>"); 592 return sb.toString(); 593 } 594 } 595