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