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