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