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