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