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