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.adservices.service.common;
18 
19 import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
20 import static org.xmlpull.v1.XmlPullParser.END_TAG;
21 import static org.xmlpull.v1.XmlPullParser.START_TAG;
22 
23 import android.annotation.NonNull;
24 import android.content.res.Resources;
25 import android.content.res.XmlResourceParser;
26 
27 import androidx.annotation.Nullable;
28 
29 import com.android.modules.utils.build.SdkLevel;
30 
31 import org.xmlpull.v1.XmlPullParserException;
32 
33 import java.io.IOException;
34 import java.util.NoSuchElementException;
35 import java.util.Objects;
36 import java.util.Optional;
37 
38 /** Parsing utils for AndroidManifest XML. */
39 public final class AndroidManifestConfigParser {
40     private static final String APPLICATION_TAG = "application";
41     private static final String PROPERTY_TAG = "property";
42     private static final String NAME_ATTRIBUTE = "name";
43     private static final String RESOURCE_ATTRIBUTE = "resource";
44     private static final String RESOLVED_RESOURCE_ID_PREFIX = "@";
45     // The <property> is strictly expected to reside inside <application>, which must be defined
46     // inside <manifest>. Therefore, the expected depth for the AdServices config property is 3.
47     private static final int AD_SERVICES_CONFIG_PROPERTY_DEPTH = 3;
48 
AndroidManifestConfigParser()49     private AndroidManifestConfigParser() {
50         // Prevent instantiation.
51     }
52 
53     /**
54      * Parses an app's compiled AndroidManifest XML resource to obtain AdServices config resource
55      * ID. The property is expected to be defined strictly inside <application>, which can only be
56      * defined inside <manifest> at an overall element depth of 3 in the XML file. The parser is
57      * intentionally meant to be lightweight and doesn't attempt to validate anything in the XML
58      * file apart from the AdServices config property itself. Returns {@code null} if AdServices
59      * config's resource ID cannot be detected in the XML.
60      *
61      * @param parser the XmlResourceParser representing the app's AndroidManifest compiled XML file.
62      * @param resources the resources belonging to the app.
63      */
64     @Nullable
getAdServicesConfigResourceId( @onNull XmlResourceParser parser, @NonNull Resources resources)65     public static Integer getAdServicesConfigResourceId(
66             @NonNull XmlResourceParser parser, @NonNull Resources resources)
67             throws XmlPullParserException, IOException {
68         if (SdkLevel.isAtLeastS()) {
69             throw new IllegalStateException(
70                     "Attempt to custom parse AndroidManifest on Android S+. Use "
71                             + "PackageManager::getProperty to parse property instead!");
72         }
73         Objects.requireNonNull(parser);
74         Objects.requireNonNull(resources);
75 
76         boolean isInsideApplication = false;
77         int eventType = parser.next();
78         while (eventType != END_DOCUMENT && !hasReachedEndOfApplication(parser)) {
79             if (eventType == START_TAG) {
80                 if (!isInsideApplication && isTagType(parser, APPLICATION_TAG)) {
81                     isInsideApplication = true;
82                 } else if (isInsideApplication
83                         && parser.getDepth() == AD_SERVICES_CONFIG_PROPERTY_DEPTH
84                         && isTagType(parser, PROPERTY_TAG)) {
85                     Optional<Integer> maybeResourceId =
86                             getResourceIdIfAdServicesProperty(parser, resources);
87                     if (maybeResourceId.isPresent()) return maybeResourceId.get();
88                 }
89             }
90             eventType = parser.next();
91         }
92 
93         return null;
94     }
95 
hasReachedEndOfApplication(@onNull XmlResourceParser parser)96     private static boolean hasReachedEndOfApplication(@NonNull XmlResourceParser parser)
97             throws XmlPullParserException {
98         return parser.getEventType() == END_TAG && isTagType(parser, APPLICATION_TAG);
99     }
100 
isTagType(@onNull XmlResourceParser parser, @NonNull String tag)101     private static boolean isTagType(@NonNull XmlResourceParser parser, @NonNull String tag) {
102         return tag.equals(parser.getName());
103     }
104 
getResourceIdIfAdServicesProperty( @onNull XmlResourceParser parser, @NonNull Resources resources)105     private static Optional<Integer> getResourceIdIfAdServicesProperty(
106             @NonNull XmlResourceParser parser, @NonNull Resources resources) {
107         Optional<String> resourceValue = Optional.empty();
108         boolean isAdServicesProperty = false;
109         final int numAttributes = parser.getAttributeCount();
110         for (int attrIndex = 0; attrIndex < numAttributes; attrIndex++) {
111             if (isAttributeType(parser, NAME_ATTRIBUTE, attrIndex)) {
112                 final String attributeVal = parser.getAttributeValue(attrIndex);
113                 isAdServicesProperty = isAdServicesPropertyName(attributeVal, resources);
114                 if (!isAdServicesProperty) break;
115             } else if (isAttributeType(parser, RESOURCE_ATTRIBUTE, attrIndex)) {
116                 final String attributeVal = parser.getAttributeValue(attrIndex);
117                 resourceValue = Optional.ofNullable(attributeVal);
118             }
119 
120             // Already extracted raw resource value belonging to AdServices config property.
121             // No need to check other attributes.
122             if (isAdServicesProperty && resourceValue.isPresent()) break;
123         }
124 
125         if (!isAdServicesProperty) return Optional.empty();
126 
127         if (resourceValue.isEmpty()) {
128             throw new NoSuchElementException(
129                     "Missing resource attribute in AdServices config property!");
130         }
131 
132         final String rawResourceVal = resourceValue.get();
133         Objects.requireNonNull(rawResourceVal);
134         final Optional<Integer> resId = maybeGetResolvedResourceId(rawResourceVal.strip());
135         if (resId.isEmpty()) {
136             throw new IllegalStateException(
137                     "AdServices config property resource not resolved to a resource ID!");
138         }
139 
140         return resId;
141     }
142 
isAttributeType( @onNull XmlResourceParser parser, @NonNull String attr, int attrIndex)143     private static boolean isAttributeType(
144             @NonNull XmlResourceParser parser, @NonNull String attr, int attrIndex) {
145         return attr.equals(parser.getAttributeName(attrIndex));
146     }
147 
isAdServicesPropertyName( @ullable String name, @NonNull Resources resources)148     private static boolean isAdServicesPropertyName(
149             @Nullable String name, @NonNull Resources resources) {
150         if (name == null) return false;
151         final String resolvedName = resolvePropertyName(name.strip(), resources);
152         return AppManifestConfigHelper.AD_SERVICES_CONFIG_PROPERTY.equals(resolvedName);
153     }
154 
155     /**
156      * The android:name value can be defined in one of two ways: <property
157      * android:name="@string/property_name_ref" .. /> OR <property android:name="property_name" ../>
158      *
159      * <p>For option 1, property_name_ref is expected to be resolved to a resource ID in the form of
160      * "@<resourceId>" where resourceId is an integer. This will require an additional step in
161      * resolving the resourceId to the property_name itself.
162      *
163      * <p>In the case of option 2, we can return property_name as-is.
164      *
165      * @param name value of android:name to be processed.
166      * @param resources resources for the app.
167      */
resolvePropertyName(@onNull String name, @NonNull Resources resources)168     private static String resolvePropertyName(@NonNull String name, @NonNull Resources resources) {
169         final Optional<Integer> resId = maybeGetResolvedResourceId(name);
170         return resId.isPresent() ? resources.getString(resId.get()) : name;
171     }
172 
maybeGetResolvedResourceId(@onNull String value)173     private static Optional<Integer> maybeGetResolvedResourceId(@NonNull String value) {
174         try {
175             // Values that are resolved to resource IDs in the compiled XML will look like
176             // "@902323". Ensure raw value can be converted into an int after omitting "@".
177             if (value.length() <= 1 || !value.startsWith(RESOLVED_RESOURCE_ID_PREFIX)) {
178                 return Optional.empty();
179             }
180             return Optional.of(Integer.parseInt(value.substring(1)));
181         } catch (Exception e) {
182             return Optional.empty();
183         }
184     }
185 }
186