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.os.Build.VERSION_CODES.TIRAMISU
20 import com.android.resources.ResourceFolderType
21 import com.android.tools.lint.detector.api.Category
22 import com.android.tools.lint.detector.api.Context
23 import com.android.tools.lint.detector.api.Detector
24 import com.android.tools.lint.detector.api.Implementation
25 import com.android.tools.lint.detector.api.Issue
26 import com.android.tools.lint.detector.api.Location
27 import com.android.tools.lint.detector.api.OtherFileScanner
28 import com.android.tools.lint.detector.api.Scope
29 import com.android.tools.lint.detector.api.Severity
30 import java.io.IOException
31 import javax.xml.XMLConstants
32 import javax.xml.transform.stream.StreamSource
33 import javax.xml.validation.SchemaFactory
34 import org.xml.sax.SAXException
35 
36 /** Lint check for detecting invalid Safety Center configs */
37 class ConfigSchemaDetector : Detector(), OtherFileScanner {
38 
39     companion object {
40         val ISSUE =
41             Issue.create(
42                 id = "InvalidSafetyCenterConfigSchema",
43                 briefDescription = "The Safety Center config does not meet the schema requirements",
44                 explanation =
45                     """The Safety Center config must follow all constraints defined in \
46                 safety_center_config.xsd. Either the config is invalid or the schema is not up to
47                 date.""",
48                 category = Category.CORRECTNESS,
49                 severity = Severity.ERROR,
50                 implementation =
51                     Implementation(ConfigSchemaDetector::class.java, Scope.OTHER_SCOPE),
52                 androidSpecific = true
53             )
54     }
55 
appliesTonull56     override fun appliesTo(folderType: ResourceFolderType): Boolean {
57         return folderType == ResourceFolderType.RAW
58     }
59 
runnull60     override fun run(context: Context) {
61         if (context.file.name != "safety_center_config.xml") {
62             return
63         }
64         val fileSdk = FileSdk.getSdkQualifier(context.file)
65         // A config must comply with the schema at the highest SDK level that is lower or equal to
66         // the SDK level of the config itself.
67         var found = false
68         for (sdk in fileSdk downTo TIRAMISU) {
69             if (testSchema(sdk, context)) {
70                 found = true
71                 break
72             }
73         }
74         if (!found) {
75             context.report(
76                 ISSUE,
77                 Location.create(context.file),
78                 "No schema found for SDK level: $fileSdk, was it deleted?"
79             )
80         }
81         // Test new schemas for backward compatibility.
82         for (sdk in fileSdk + 1..FileSdk.getMaxSdkVersion()) {
83             testSchema(sdk, context)
84         }
85     }
86 
testSchemanull87     private fun testSchema(sdk: Int, context: Context): Boolean {
88         val xsdInputStream = FileSdk.getSchemaAsStream(sdk) ?: return false
89         val xsd = StreamSource(xsdInputStream)
90         val xml = StreamSource(context.file.inputStream())
91         val schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
92         try {
93             val schema = schemaFactory.newSchema(xsd)
94             val validator = schema.newValidator()
95             validator.validate(xml)
96         } catch (e: SAXException) {
97             context.report(
98                 ISSUE,
99                 Location.create(context.file),
100                 "SAXException exception at sdk=$sdk: \"${e.message}\""
101             )
102         } catch (e: IOException) {
103             context.report(
104                 ISSUE,
105                 Location.create(context.file),
106                 "IOException exception at sdk=$sdk: \"${e.message}\""
107             )
108         }
109         return true
110     }
111 }
112