1 /*
2  * Copyright (C) 2022 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 android.safetycenter.lint
18 
19 import android.content.res.Resources
20 import com.android.SdkConstants.ATTR_NAME
21 import com.android.SdkConstants.TAG_STRING
22 import com.android.modules.utils.build.SdkLevel
23 import com.android.resources.ResourceFolderType
24 import com.android.safetycenter.config.ParseException
25 import com.android.safetycenter.config.SafetyCenterConfigParser
26 import com.android.tools.lint.detector.api.Category
27 import com.android.tools.lint.detector.api.Context
28 import com.android.tools.lint.detector.api.Detector
29 import com.android.tools.lint.detector.api.Implementation
30 import com.android.tools.lint.detector.api.Issue
31 import com.android.tools.lint.detector.api.Location
32 import com.android.tools.lint.detector.api.OtherFileScanner
33 import com.android.tools.lint.detector.api.Scope
34 import com.android.tools.lint.detector.api.Severity
35 import com.android.tools.lint.detector.api.XmlContext
36 import com.android.tools.lint.detector.api.XmlScanner
37 import java.util.EnumSet
38 import kotlin.math.min
39 import org.w3c.dom.Element
40 import org.w3c.dom.Node
41 
42 /** Lint check for detecting invalid Safety Center configs */
43 class ParserExceptionDetector : Detector(), OtherFileScanner, XmlScanner {
44 
45     companion object {
46         val ISSUE =
47             Issue.create(
48                 id = "InvalidSafetyCenterConfig",
49                 briefDescription = "The Safety Center config parser detected an error",
50                 explanation =
51                     """The Safety Center config must follow all constraints defined in \
52                 safety_center_config.xsd. Check the error message to find out the specific \
53                 constraint not met by the current config.""",
54                 category = Category.CORRECTNESS,
55                 severity = Severity.ERROR,
56                 implementation =
57                     Implementation(
58                         ParserExceptionDetector::class.java,
59                         EnumSet.of(Scope.RESOURCE_FILE, Scope.OTHER)
60                     ),
61                 androidSpecific = true
62             )
63 
64         const val STRING_MAP_BUILD_PHASE = 1
65         const val CONFIG_PARSE_PHASE = 2
66     }
67 
appliesTonull68     override fun appliesTo(folderType: ResourceFolderType): Boolean {
69         return folderType == ResourceFolderType.RAW || folderType == ResourceFolderType.VALUES
70     }
71 
afterCheckEachProjectnull72     override fun afterCheckEachProject(context: Context) {
73         context.driver.requestRepeat(this, Scope.OTHER_SCOPE)
74     }
75 
76     /** Implements XmlScanner and builds a map of string resources in the first phase */
77     private val mNameToIndex: MutableMap<String, Int> = mutableMapOf()
78     private val mIndexToValue: MutableMap<Int, String> = mutableMapOf()
79     private val mIndexToMinSdk: MutableMap<Int, Int> = mutableMapOf()
80     private var mIndex = 1000
81 
getApplicableElementsnull82     override fun getApplicableElements(): Collection<String>? {
83         return listOf(TAG_STRING)
84     }
85 
visitElementnull86     override fun visitElement(context: XmlContext, element: Element) {
87         if (
88             context.driver.phase != STRING_MAP_BUILD_PHASE ||
89                 context.resourceFolderType != ResourceFolderType.VALUES ||
90                 !FileSdk.belongsToABasicConfiguration(context.file)
91         ) {
92             return
93         }
94         val minSdk = FileSdk.getSdkQualifier(context.file)
95         val name = element.getAttribute(ATTR_NAME)
96         val index = mNameToIndex[name]
97         if (index != null) {
98             mIndexToMinSdk[index] = min(mIndexToMinSdk[index]!!, minSdk)
99             return
100         }
101         var value = ""
102         for (childIndex in 0 until element.childNodes.length) {
103             val child = element.childNodes.item(childIndex)
104             if (child.nodeType == Node.TEXT_NODE) {
105                 value = child.nodeValue
106                 break
107             }
108         }
109         mNameToIndex[name] = mIndex
110         mIndexToValue[mIndex] = value
111         mIndexToMinSdk[mIndex] = minSdk
112         mIndex++
113     }
114 
115     /** Implements OtherFileScanner and parses the XML config in the second phase */
runnull116     override fun run(context: Context) {
117         if (
118             context.driver.phase != CONFIG_PARSE_PHASE ||
119                 context.file.name != "safety_center_config.xml"
120         ) {
121             return
122         }
123         val minSdk = FileSdk.getSdkQualifier(context.file)
124         val maxSdk = maxOf(minSdk, FileSdk.getMaxSdkVersion())
125         // Test the parser at the SDK level for which the config was designed.
126         // Then test parsers at higher SDK levels for backward compatibility.
127         // This is slightly inefficient if a parser at a higher SDK level has no behavioral changes
128         // compared to one at a lower SDK level, but doing an exhaustive search is safer.
129         for (sdk in minSdk..maxSdk) {
130             synchronized(SdkLevel::class.java) {
131                 SdkLevel.setSdkInt(sdk)
132                 try {
133                     SafetyCenterConfigParser.parseXmlResource(
134                         context.file.inputStream(),
135                         // Note: using a map of the string resources present in the APK under
136                         // analysis is necessary in order to get the value of string resources that
137                         // are resolved and validated at parse time. The drawback of this is that
138                         // the linter cannot be used on overlay packages that refer to resources in
139                         // the target package or on packages that refer to Android global resources.
140                         // However, we cannot use a custom linter with the default soong overlay
141                         // build rule regardless.
142                         Resources(
143                             context.project.`package`,
144                             mNameToIndex.filterValues { sdk >= mIndexToMinSdk[it]!! },
145                             mIndexToValue.filterKeys { sdk >= mIndexToMinSdk[it]!! }
146                         )
147                     )
148                 } catch (e: ParseException) {
149                     context.report(
150                         ISSUE,
151                         Location.create(context.file),
152                         "Parser exception at sdk=$sdk: \"${e.message}\", cause: " +
153                             "\"${e.cause?.message}\""
154                     )
155                 }
156             }
157         }
158     }
159 }
160