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