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.server.pm;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.util.Slog;
22 import android.util.TypedXmlPullParser;
23 import android.util.TypedXmlSerializer;
24 import android.util.Xml;
25 
26 import org.xmlpull.v1.XmlPullParser;
27 import org.xmlpull.v1.XmlPullParserException;
28 
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.nio.charset.StandardCharsets;
32 import java.util.Stack;
33 
34 /**
35  * A very specialized serialization/parsing wrapper around {@link TypedXmlSerializer} and {@link
36  * TypedXmlPullParser} intended for use with PackageManager related settings files.
37  * Assumptions/chosen behaviors:
38  * <ul>
39  *     <li>No namespace support</li>
40  *     <li>Data for a parent object is stored as attributes</li>
41  *     <li>All attribute read methods return a default false, -1, or null</li>
42  *     <li>Default values will not be written</li>
43  *     <li>Children are sub-elements</li>
44  *     <li>Collections are repeated sub-elements, no attribute support for collections</li>
45  * </ul>
46  */
47 public class SettingsXml {
48 
49     private static final String TAG = "SettingsXml";
50 
51     private static final boolean DEBUG_THROW_EXCEPTIONS = false;
52 
53     private static final String FEATURE_INDENT =
54             "http://xmlpull.org/v1/doc/features.html#indent-output";
55 
56     private static final int DEFAULT_NUMBER = -1;
57 
serializer(TypedXmlSerializer serializer)58     public static Serializer serializer(TypedXmlSerializer serializer) {
59         return new Serializer(serializer);
60     }
61 
parser(TypedXmlPullParser parser)62     public static ReadSection parser(TypedXmlPullParser parser)
63             throws IOException, XmlPullParserException {
64         return new ReadSectionImpl(parser);
65     }
66 
67     public static class Serializer implements AutoCloseable {
68 
69         @NonNull
70         private final TypedXmlSerializer mXmlSerializer;
71 
72         private final WriteSectionImpl mWriteSection;
73 
Serializer(TypedXmlSerializer serializer)74         private Serializer(TypedXmlSerializer serializer) {
75             mXmlSerializer = serializer;
76             mWriteSection = new WriteSectionImpl(mXmlSerializer);
77         }
78 
startSection(@onNull String sectionName)79         public WriteSection startSection(@NonNull String sectionName) throws IOException {
80             return mWriteSection.startSection(sectionName);
81         }
82 
83         @Override
close()84         public void close() throws IOException {
85             mWriteSection.closeCompletely();
86             mXmlSerializer.flush();
87         }
88     }
89 
90     public interface ReadSection extends AutoCloseable {
91 
92         @NonNull
getName()93         String getName();
94 
95         @NonNull
getDescription()96         String getDescription();
97 
has(String attrName)98         boolean has(String attrName);
99 
100         @Nullable
getString(String attrName)101         String getString(String attrName);
102 
103         /**
104          * @return value as String or {@param defaultValue} if doesn't exist
105          */
106         @NonNull
getString(String attrName, @NonNull String defaultValue)107         String getString(String attrName, @NonNull String defaultValue);
108 
109         /**
110          * @return value as boolean or false if doesn't exist
111          */
getBoolean(String attrName)112         boolean getBoolean(String attrName);
113 
114         /**
115          * @return value as boolean or {@param defaultValue} if doesn't exist
116          */
getBoolean(String attrName, boolean defaultValue)117         boolean getBoolean(String attrName, boolean defaultValue);
118 
119         /**
120          * @return value as int or {@link #DEFAULT_NUMBER} if doesn't exist
121          */
getInt(String attrName)122         int getInt(String attrName);
123 
124         /**
125          * @return value as int or {@param defaultValue} if doesn't exist
126          */
getInt(String attrName, int defaultValue)127         int getInt(String attrName, int defaultValue);
128 
129         /**
130          * @return value as long or {@link #DEFAULT_NUMBER} if doesn't exist
131          */
getLong(String attrName)132         long getLong(String attrName);
133 
134         /**
135          * @return value as long or {@param defaultValue} if doesn't exist
136          */
getLong(String attrName, int defaultValue)137         long getLong(String attrName, int defaultValue);
138 
children()139         ChildSection children();
140     }
141 
142     /**
143      * <pre><code>
144      * ChildSection child = parentSection.children();
145      * while (child.moveToNext(TAG_CHILD)) {
146      *     String readValue = child.getString(...);
147      *     ...
148      * }
149      * </code></pre>
150      */
151     public interface ChildSection extends ReadSection {
moveToNext()152         boolean moveToNext();
153 
moveToNext(@onNull String expectedChildTagName)154         boolean moveToNext(@NonNull String expectedChildTagName);
155     }
156 
157     public static class ReadSectionImpl implements ChildSection {
158 
159         @Nullable
160         private final InputStream mInput;
161 
162         @NonNull
163         private final TypedXmlPullParser mParser;
164 
165         @NonNull
166         private final Stack<Integer> mDepthStack = new Stack<>();
167 
ReadSectionImpl(@onNull InputStream input)168         public ReadSectionImpl(@NonNull InputStream input)
169                 throws IOException, XmlPullParserException {
170             mInput = input;
171             mParser = Xml.newFastPullParser();
172             mParser.setInput(mInput, StandardCharsets.UTF_8.name());
173             moveToFirstTag();
174         }
175 
ReadSectionImpl(@onNull TypedXmlPullParser parser)176         public ReadSectionImpl(@NonNull TypedXmlPullParser parser)
177                 throws IOException, XmlPullParserException {
178             mInput = null;
179             mParser = parser;
180             moveToFirstTag();
181         }
182 
moveToFirstTag()183         private void moveToFirstTag() throws IOException, XmlPullParserException {
184             if (mParser.getEventType() == XmlPullParser.START_TAG) {
185                 return;
186             }
187 
188             int type;
189             //noinspection StatementWithEmptyBody
190             while ((type = mParser.next()) != XmlPullParser.START_TAG
191                     && type != XmlPullParser.END_DOCUMENT) {
192             }
193         }
194 
195         @NonNull
196         @Override
getName()197         public String getName() {
198             return mParser.getName();
199         }
200 
201         @NonNull
202         @Override
getDescription()203         public String getDescription() {
204             return mParser.getPositionDescription();
205         }
206 
207         @Override
has(String attrName)208         public boolean has(String attrName) {
209             return mParser.getAttributeValue(null, attrName) != null;
210         }
211 
212         @Nullable
213         @Override
getString(String attrName)214         public String getString(String attrName) {
215             return mParser.getAttributeValue(null, attrName);
216         }
217 
218         @NonNull
219         @Override
getString(String attrName, @NonNull String defaultValue)220         public String getString(String attrName, @NonNull String defaultValue) {
221             String value = mParser.getAttributeValue(null, attrName);
222             if (value == null) {
223                 return defaultValue;
224             }
225             return value;
226         }
227 
228         @Override
getBoolean(String attrName)229         public boolean getBoolean(String attrName) {
230             return getBoolean(attrName, false);
231         }
232 
233         @Override
getBoolean(String attrName, boolean defaultValue)234         public boolean getBoolean(String attrName, boolean defaultValue) {
235             return mParser.getAttributeBoolean(null, attrName, defaultValue);
236         }
237 
238         @Override
getInt(String attrName)239         public int getInt(String attrName) {
240             return getInt(attrName, DEFAULT_NUMBER);
241         }
242 
243         @Override
getInt(String attrName, int defaultValue)244         public int getInt(String attrName, int defaultValue) {
245             return mParser.getAttributeInt(null, attrName, defaultValue);
246         }
247 
248         @Override
getLong(String attrName)249         public long getLong(String attrName) {
250             return getLong(attrName, DEFAULT_NUMBER);
251         }
252 
253         @Override
getLong(String attrName, int defaultValue)254         public long getLong(String attrName, int defaultValue) {
255             return mParser.getAttributeLong(null, attrName, defaultValue);
256         }
257 
258         @Override
children()259         public ChildSection children() {
260             mDepthStack.push(mParser.getDepth());
261             return this;
262         }
263 
264         @Override
moveToNext()265         public boolean moveToNext() {
266             return moveToNextInternal(null);
267         }
268 
269         @Override
moveToNext(@onNull String expectedChildTagName)270         public boolean moveToNext(@NonNull String expectedChildTagName) {
271             return moveToNextInternal(expectedChildTagName);
272         }
273 
moveToNextInternal(@ullable String expectedChildTagName)274         private boolean moveToNextInternal(@Nullable String expectedChildTagName) {
275             try {
276                 int depth = mDepthStack.peek();
277                 boolean hasTag = false;
278                 int type;
279                 while (!hasTag
280                         && (type = mParser.next()) != XmlPullParser.END_DOCUMENT
281                         && (type != XmlPullParser.END_TAG || mParser.getDepth() > depth)) {
282                     if (type != XmlPullParser.START_TAG) {
283                         continue;
284                     }
285 
286                     if (expectedChildTagName != null
287                             && !expectedChildTagName.equals(mParser.getName())) {
288                         continue;
289                     }
290 
291                     hasTag = true;
292                 }
293 
294                 if (!hasTag) {
295                     mDepthStack.pop();
296                 }
297 
298                 return hasTag;
299             } catch (Exception ignored) {
300                 return false;
301             }
302         }
303 
304         @Override
close()305         public void close() throws Exception {
306             if (mDepthStack.isEmpty()) {
307                 Slog.wtf(TAG, "Children depth stack was not empty, data may have been lost",
308                         new Exception());
309             }
310             if (mInput != null) {
311                 mInput.close();
312             }
313         }
314     }
315 
316     public interface WriteSection extends AutoCloseable {
317 
startSection(@onNull String sectionName)318         WriteSection startSection(@NonNull String sectionName) throws IOException;
319 
attribute(String attrName, @Nullable String value)320         WriteSection attribute(String attrName, @Nullable String value) throws IOException;
321 
attribute(String attrName, int value)322         WriteSection attribute(String attrName, int value) throws IOException;
323 
attribute(String attrName, long value)324         WriteSection attribute(String attrName, long value) throws IOException;
325 
attribute(String attrName, boolean value)326         WriteSection attribute(String attrName, boolean value) throws IOException;
327 
328         @Override
close()329         void close() throws IOException;
330 
finish()331         void finish() throws IOException;
332     }
333 
334     private static class WriteSectionImpl implements WriteSection {
335 
336         @NonNull
337         private final TypedXmlSerializer mXmlSerializer;
338 
339         @NonNull
340         private final Stack<String> mTagStack = new Stack<>();
341 
WriteSectionImpl(@onNull TypedXmlSerializer xmlSerializer)342         private WriteSectionImpl(@NonNull TypedXmlSerializer xmlSerializer) {
343             mXmlSerializer = xmlSerializer;
344         }
345 
346         @Override
startSection(@onNull String sectionName)347         public WriteSection startSection(@NonNull String sectionName) throws IOException {
348             // Try to start the tag first before we push it to the stack
349             mXmlSerializer.startTag(null, sectionName);
350             mTagStack.push(sectionName);
351             return this;
352         }
353 
354         @Override
attribute(String attrName, String value)355         public WriteSection attribute(String attrName, String value) throws IOException {
356             if (value != null) {
357                 mXmlSerializer.attribute(null, attrName, value);
358             }
359             return this;
360         }
361 
362         @Override
attribute(String attrName, int value)363         public WriteSection attribute(String attrName, int value) throws IOException {
364             if (value != DEFAULT_NUMBER) {
365                 mXmlSerializer.attributeInt(null, attrName, value);
366             }
367             return this;
368         }
369 
370         @Override
attribute(String attrName, long value)371         public WriteSection attribute(String attrName, long value) throws IOException {
372             if (value != DEFAULT_NUMBER) {
373                 mXmlSerializer.attributeLong(null, attrName, value);
374             }
375             return this;
376         }
377 
378         @Override
attribute(String attrName, boolean value)379         public WriteSection attribute(String attrName, boolean value) throws IOException {
380             if (value) {
381                 mXmlSerializer.attributeBoolean(null, attrName, value);
382             }
383             return this;
384         }
385 
386         @Override
finish()387         public void finish() throws IOException {
388             close();
389         }
390 
391         @Override
close()392         public void close() throws IOException {
393             mXmlSerializer.endTag(null, mTagStack.pop());
394         }
395 
closeCompletely()396         private void closeCompletely() throws IOException {
397             if (DEBUG_THROW_EXCEPTIONS && mTagStack != null && !mTagStack.isEmpty()) {
398                 throw new IllegalStateException(
399                         "tag stack is not empty when closing, contains " + mTagStack);
400             } else if (mTagStack != null) {
401                 while (!mTagStack.isEmpty()) {
402                     close();
403                 }
404             }
405         }
406     }
407 }
408