1 /*
2  * Copyright (C) 2023 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.healthconnect.permission;
18 
19 import android.annotation.NonNull;
20 import android.util.ArrayMap;
21 import android.util.AtomicFile;
22 import android.util.Log;
23 import android.util.Xml;
24 
25 import libcore.io.IoUtils;
26 
27 import org.xmlpull.v1.XmlPullParser;
28 import org.xmlpull.v1.XmlPullParserException;
29 import org.xmlpull.v1.XmlSerializer;
30 
31 import java.io.File;
32 import java.io.FileInputStream;
33 import java.io.FileNotFoundException;
34 import java.io.FileOutputStream;
35 import java.io.IOException;
36 import java.nio.charset.StandardCharsets;
37 import java.time.Instant;
38 import java.util.Map;
39 
40 /**
41  * Helper class for serialisation / parsing grant time xml file.
42  *
43  * @hide
44  */
45 public final class GrantTimeXmlHelper {
46     private static final String TAG = "GrantTimeSerializer";
47     private static final String TAG_FIRST_GRANT_TIMES = "first-grant-times";
48     private static final String TAG_PACKAGE = "package";
49     private static final String TAG_SHARED_USER = "shared-user";
50 
51     private static final String ATTRIBUTE_NAME = "name";
52     private static final String ATTRIBUTE_FIRST_GRANT_TIME = "first-grant-time";
53     private static final String ATTRIBUTE_VERSION = "version";
54 
55     /**
56      * Serializes the grant times into the passed file.
57      *
58      * @param userGrantTimeState the grant times to be serialized.
59      * @param file the file into which the serialized data should be written.
60      */
serializeGrantTimes( @onNull File file, @NonNull UserGrantTimeState userGrantTimeState)61     public static void serializeGrantTimes(
62             @NonNull File file, @NonNull UserGrantTimeState userGrantTimeState) {
63         AtomicFile atomicFile = new AtomicFile(file);
64         FileOutputStream outputStream = null;
65         try {
66             outputStream = atomicFile.startWrite();
67 
68             XmlSerializer serializer = Xml.newSerializer();
69             serializer.setOutput(outputStream, StandardCharsets.UTF_8.name());
70             serializer.startDocument(/* encoding= */ null, /* standalone= */ true);
71             GrantTimeXmlHelper.writeGrantTimes(serializer, userGrantTimeState);
72 
73             serializer.endDocument();
74             atomicFile.finishWrite(outputStream);
75         } catch (Exception e) {
76             Log.wtf(TAG, "Failed to write, restoring backup: " + file, e);
77             atomicFile.failWrite(outputStream);
78         } finally {
79             IoUtils.closeQuietly(outputStream);
80         }
81     }
82 
83     /**
84      * Parses the passed grant time file to return the grant times.
85      *
86      * @param file the file from which the data should be parsed.
87      * @return the grant times.
88      */
89     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
parseGrantTime(File file)90     public static UserGrantTimeState parseGrantTime(File file) {
91         try (FileInputStream inputStream = new AtomicFile(file).openRead()) {
92             XmlPullParser parser = Xml.newPullParser();
93             parser.setInput(inputStream, /* inputEncoding= */ null);
94             return parseXml(parser);
95         } catch (FileNotFoundException e) {
96             Log.w(TAG, file.getPath() + " not found");
97             return null;
98         } catch (XmlPullParserException | IOException e) {
99             throw new IllegalStateException("Failed to read " + file, e);
100         }
101     }
102 
103     @NonNull
parseXml(@onNull XmlPullParser parser)104     private static UserGrantTimeState parseXml(@NonNull XmlPullParser parser)
105             throws IOException, XmlPullParserException {
106         int targetDepth = parser.getDepth() + 1;
107         int type = parser.next();
108 
109         // Scan the xml until find the grant time tag at the target depth.
110         while (type != XmlPullParser.END_DOCUMENT
111                 && (parser.getDepth() >= targetDepth || type != XmlPullParser.END_TAG)) {
112             if (parser.getDepth() > targetDepth || type != XmlPullParser.START_TAG) {
113                 type = parser.next();
114                 continue;
115             }
116 
117             if (parser.getName().equals(TAG_FIRST_GRANT_TIMES)) {
118                 return parseFirstGrantTimes(parser);
119             }
120 
121             type = parser.next();
122         }
123         throw new IllegalStateException(
124                 "Missing <" + TAG_FIRST_GRANT_TIMES + "> in provided file.");
125     }
126 
writeGrantTimes( @onNull XmlSerializer serializer, @NonNull UserGrantTimeState userGrantTimeState)127     private static void writeGrantTimes(
128             @NonNull XmlSerializer serializer, @NonNull UserGrantTimeState userGrantTimeState)
129             throws IOException {
130         serializer.startTag(/* namespace= */ null, TAG_FIRST_GRANT_TIMES);
131         serializer.attribute(
132                 /* namespace= */ null,
133                 ATTRIBUTE_VERSION,
134                 Integer.toString(userGrantTimeState.getVersion()));
135 
136         for (Map.Entry<String, Instant> entry :
137                 userGrantTimeState.getPackageGrantTimes().entrySet()) {
138             String packageName = entry.getKey();
139             Instant grantTime = entry.getValue();
140 
141             serializer.startTag(/* namespace= */ null, TAG_PACKAGE);
142             serializer.attribute(/* namespace= */ null, ATTRIBUTE_NAME, packageName);
143             serializer.attribute(
144                     /* namespace= */ null, ATTRIBUTE_FIRST_GRANT_TIME, grantTime.toString());
145             serializer.endTag(/* namespace= */ null, TAG_PACKAGE);
146         }
147 
148         for (Map.Entry<String, Instant> entry :
149                 userGrantTimeState.getSharedUserGrantTimes().entrySet()) {
150             String sharedUserName = entry.getKey();
151             Instant grantTime = entry.getValue();
152 
153             serializer.startTag(/* namespace= */ null, TAG_SHARED_USER);
154             serializer.attribute(/* namespace= */ null, ATTRIBUTE_NAME, sharedUserName);
155             serializer.attribute(
156                     /* namespace= */ null, ATTRIBUTE_FIRST_GRANT_TIME, grantTime.toString());
157             serializer.endTag(/* namespace= */ null, TAG_SHARED_USER);
158         }
159 
160         serializer.endTag(/* namespace= */ null, TAG_FIRST_GRANT_TIMES);
161     }
162 
163     @NonNull
parseFirstGrantTimes(@onNull XmlPullParser parser)164     private static UserGrantTimeState parseFirstGrantTimes(@NonNull XmlPullParser parser)
165             throws IOException, XmlPullParserException {
166         String versionValue = parser.getAttributeValue(/* namespace= */ null, ATTRIBUTE_VERSION);
167         int version =
168                 versionValue != null
169                         ? Integer.parseInt(versionValue)
170                         : UserGrantTimeState.NO_VERSION;
171         Map<String, Instant> packagePermissions = new ArrayMap<>();
172         Map<String, Instant> sharedUserPermissions = new ArrayMap<>();
173 
174         int targetDepth = parser.getDepth() + 1;
175         int type = parser.next();
176         // Scan the xml until find the needed tags at the target depth.
177         while (type != XmlPullParser.END_DOCUMENT
178                 && (parser.getDepth() >= targetDepth || type != XmlPullParser.END_TAG)) {
179             if (parser.getDepth() > targetDepth || type != XmlPullParser.START_TAG) {
180                 type = parser.next();
181                 continue;
182             }
183             switch (parser.getName()) {
184                 case TAG_PACKAGE:
185                     {
186                         String packageName =
187                                 parser.getAttributeValue(/* namespace= */ null, ATTRIBUTE_NAME);
188                         Instant firstGrantTime =
189                                 Instant.parse(
190                                         parser.getAttributeValue(
191                                                 /* namespace= */ null, ATTRIBUTE_FIRST_GRANT_TIME));
192                         packagePermissions.put(packageName, firstGrantTime);
193                         break;
194                     }
195                 case TAG_SHARED_USER:
196                     {
197                         String sharedUserName =
198                                 parser.getAttributeValue(/* namespace= */ null, ATTRIBUTE_NAME);
199                         Instant firstGrantTime =
200                                 Instant.parse(
201                                         parser.getAttributeValue(
202                                                 /* namespace= */ null, ATTRIBUTE_FIRST_GRANT_TIME));
203                         sharedUserPermissions.put(sharedUserName, firstGrantTime);
204                         break;
205                     }
206                 default:
207                     {
208                         Log.w(TAG, "Tag " + parser.getName() + " is not parsed");
209                     }
210             }
211             type = parser.next();
212         }
213 
214         return new UserGrantTimeState(packagePermissions, sharedUserPermissions, version);
215     }
216 }
217