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